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" "source": "${PROJECT_SOURCE_DIR}/data/fpga.bit"
}, },
{ {
"type": "db", "type": "gamedb",
"name": "data/games.db", "name": "data/games.db",
"source": "${PROJECT_SOURCE_DIR}/data/games.json" "source": "${PROJECT_SOURCE_DIR}/data/games.json"
}, },

View File

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

View File

@ -23,7 +23,7 @@
"properties": { "properties": {
"type": { "type": {
"title": "Entry 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", "type": "string",
"enum": [ "enum": [
@ -34,7 +34,7 @@
"metrics", "metrics",
"palette", "palette",
"strings", "strings",
"db" "gamedb"
] ]
}, },
"name": { "name": {
@ -172,7 +172,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "pattern": "^metrics|palette|strings|db$" }, "type": { "pattern": "^metrics|palette|strings|gamedb$" },
"name": { "type": "string" }, "name": { "type": "string" },
"compLevel": {}, "compLevel": {},
@ -234,18 +234,18 @@
} }
}, },
{ {
"required": [ "db" ], "required": [ "gamedb" ],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "const": "db" }, "type": { "const": "gamedb" },
"name": { "type": "string" }, "name": { "type": "string" },
"compLevel": {}, "compLevel": {},
"strings": { "strings": {
"title": "Game database", "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.", "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)) if (!output.allocate(uncompLength + margin))
return 0; return 0;
auto compPtr = &output.as<uint8_t>() + margin; auto compPtr = output.as<uint8_t>() + margin;
if ( if (
(_file->seek(offset) != offset) || (_file->seek(offset) != offset) ||

View File

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

View File

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

View File

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

View File

@ -91,17 +91,28 @@ enum SignatureFlag : uint8_t {
SIGNATURE_PAD_WITH_FF = 1 << 2 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 { enum GameFlag : uint8_t {
GAME_IO_BOARD_BITMASK = 7 << 0, GAME_INSTALL_RTC_HEADER_REQUIRED = 1 << 0,
GAME_IO_BOARD_NONE = 0 << 0, GAME_RTC_HEADER_REQUIRED = 1 << 1
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 database structures */ /* Game database structures */
@ -130,12 +141,15 @@ struct GameInfo {
public: public:
char specifications[MAX_SPECIFICATIONS]; char specifications[MAX_SPECIFICATIONS];
char regions[MAX_REGIONS][3]; char regions[MAX_REGIONS][3];
char code[3]; char code[4];
uint8_t flags;
uint16_t nameOffset; uint16_t nameOffset;
uint16_t year; uint16_t year;
IOBoardType ioBoard;
PCMCIADeviceType pcmcia[2];
uint8_t flags;
ROMHeaderInfo rtcHeader, flashHeader; ROMHeaderInfo rtcHeader, flashHeader;
CartInfo installCart, gameCart; CartInfo installCart, gameCart;
}; };
@ -145,15 +159,16 @@ public:
static constexpr size_t NUM_SORT_TABLES = 4; static constexpr size_t NUM_SORT_TABLES = 4;
enum SortOrder : uint8_t { enum SortOrder : uint8_t {
SORT_CODE = 0, SORT_CODE = 0,
SORT_NAME = 1, SORT_NAME = 1,
SORT_YEAR = 2 SORT_SERIES = 2,
SORT_YEAR = 3
}; };
class GameDBHeader { class GameDBHeader {
public: public:
uint32_t magic[2]; uint32_t magic[2];
uint16_t sortTableOffsets[NUM_SORT_TABLES]; uint16_t numEntries, numSortOrders;
inline bool validateMagic(void) const { inline bool validateMagic(void) const {
return (magic[0] == "573g"_c) && (magic[1] == "medb"_c); 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.analysis import MAMENVRAMDump, getBootloaderVersion
from common.cartparser import parseCartHeader, parseROMHeader from common.cartparser import parseCartHeader, parseROMHeader
from common.decompile import AnalysisError from common.decompile import AnalysisError
from common.gamedb import GameInfo from common.gamedb import GameInfo, PCMCIADeviceType
from common.util import \ from common.util import \
JSONFormatter, JSONGroupedArray, JSONGroupedObject, setupLogger JSONFormatter, JSONGroupedArray, JSONGroupedObject, setupLogger
## Game analysis ## 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): 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: if reanalyze or not game.pcmcia1:
try: game.pcmcia1 = _PCMCIA_CARD_TYPES[dump.pcmcia1Size]
game.bootloaderVersion = getBootloaderVersion(dump.bootloader) if reanalyze or not game.pcmcia2:
except AnalysisError: game.pcmcia2 = _PCMCIA_CARD_TYPES[dump.pcmcia2Size]
pass
if (reanalyze or game.rtcHeader is None) and dump.rtcHeader: if reanalyze or game.bootloaderVersion is None:
game.rtcHeader = parseROMHeader(dump.rtcHeader, True) game.bootloaderVersion = None
if (reanalyze or game.flashHeader is None) and dump.flashHeader:
game.flashHeader = parseROMHeader(dump.flashHeader)
if (reanalyze or game.installCart is None) and dump.installCart: if dump.bootloader:
game.installCart = parseCartHeader(dump.installCart) try:
if (reanalyze or game.gameCart is None) and dump.gameCart: game.bootloaderVersion = getBootloaderVersion(dump.bootloader)
game.gameCart = parseCartHeader(dump.gameCart) 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 ## Main

View File

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

View File

@ -16,6 +16,7 @@
import logging, re import logging, re
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from .cart import * from .cart import *
@ -25,16 +26,61 @@ from .util import InterleavedFile
## MAME NVRAM directory reader ## 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: 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: try:
with InterleavedFile( with InterleavedFile(
open(nvramDir / "29f016a.31m", "rb"), open(path / "29f016a.31m", "rb"),
open(nvramDir / "29f016a.27m", "rb") open(path / "29f016a.27m", "rb")
) as file: ) as file:
file.seek(FLASH_HEADER_OFFSET) file.seek(FLASH_HEADER_OFFSET)
self.flashHeader: ROMHeaderDump | None = ROMHeaderDump( flashHeader: ROMHeaderDump | None = ROMHeaderDump(
DumpFlag.DUMP_PUBLIC_DATA_OK, DumpFlag.DUMP_PUBLIC_DATA_OK,
b"", b"",
file.read(FLASH_HEADER_LENGTH) file.read(FLASH_HEADER_LENGTH)
@ -44,41 +90,27 @@ class MAMENVRAMDump:
file.seek(FLASH_EXECUTABLE_OFFSET) file.seek(FLASH_EXECUTABLE_OFFSET)
try: try:
self.bootloader: PSEXEAnalyzer | None = PSEXEAnalyzer(file) bootloader: PSEXEAnalyzer | None = PSEXEAnalyzer(file)
except AnalysisError: except AnalysisError:
self.bootloader: PSEXEAnalyzer | None = None bootloader: PSEXEAnalyzer | None = None
except FileNotFoundError: except FileNotFoundError:
self.flashHeader: ROMHeaderDump | None = None flashHeader: ROMHeaderDump | None = None
self.bootloader: PSEXEAnalyzer | None = None bootloader: PSEXEAnalyzer | None = None
try: return MAMENVRAMDump(
with open(nvramDir / "m48t58", "rb") as file: _getPCMCIACardSize(path, 1),
file.seek(RTC_HEADER_OFFSET) _getPCMCIACardSize(path, 2),
rtcHeader,
self.rtcHeader: ROMHeaderDump | None = ROMHeaderDump( flashHeader,
DumpFlag.DUMP_PUBLIC_DATA_OK, bootloader,
b"", _loadCartDump(path / "cassette_install_eeprom"),
file.read(RTC_HEADER_LENGTH) _loadCartDump(path / "cassette_game_eeprom")
) )
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
## Bootloader executable analysis ## Bootloader executable analysis
_BOOT_VERSION_REGEX: re.Pattern = \ _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: def getBootloaderVersion(exe: PSEXEAnalyzer) -> str:
for matched in _BOOT_VERSION_REGEX.finditer(exe.body): for matched in _BOOT_VERSION_REGEX.finditer(exe.body):

View File

@ -17,12 +17,13 @@
from collections.abc import Generator, Mapping, Sequence from collections.abc import Generator, Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from struct import Struct from struct import Struct
from typing import Any from typing import Any, Callable
import numpy import numpy
from numpy import ndarray from numpy import ndarray
from PIL import Image from PIL import Image
from .util import \ from .gamedb import GAME_INFO_STRUCT, GameInfo
from .util import \
HashTableBuilder, StringBlobBuilder, colorFromString, hashData, \ HashTableBuilder, StringBlobBuilder, colorFromString, hashData, \
roundUpToMultiple roundUpToMultiple
@ -263,7 +264,7 @@ def generateStringTable(
for keyHash, string in _walkStringTree(strings): for keyHash, string in _walkStringTree(strings):
hashTable.addEntry(keyHash, blob.addString(string)) hashTable.addEntry(keyHash, blob.addString(string))
tableLength: int = 0 \ blobOffset: int = 0 \
+ _STRING_TABLE_HEADER_STRUCT.size \ + _STRING_TABLE_HEADER_STRUCT.size \
+ _STRING_TABLE_ENTRY_STRUCT.size * len(hashTable.entries) + _STRING_TABLE_ENTRY_STRUCT.size * len(hashTable.entries)
@ -280,12 +281,62 @@ def generateStringTable(
else: else:
tableData += _STRING_TABLE_ENTRY_STRUCT.pack( tableData += _STRING_TABLE_ENTRY_STRUCT.pack(
entry.fullHash, entry.fullHash,
tableLength + entry.data, blobOffset + entry.data,
entry.chainIndex entry.chainIndex
) )
return tableData + blob.data 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 header generator
_PACKAGE_INDEX_HEADER_STRUCT: Struct = Struct("< 8s I 2H") _PACKAGE_INDEX_HEADER_STRUCT: Struct = Struct("< 8s I 2H")

View File

@ -24,7 +24,7 @@ from .util import JSONGroupedObject
## Utilities ## Utilities
def _makeJSONObject(*groups: Mapping[str, Any]) -> JSONGroupedObject: def _makeJSONObject(*groups: Mapping[str, Any]) -> JSONGroupedObject | None:
jsonObj: JSONGroupedObject = JSONGroupedObject() jsonObj: JSONGroupedObject = JSONGroupedObject()
for group in groups: for group in groups:
@ -37,7 +37,7 @@ def _makeJSONObject(*groups: Mapping[str, Any]) -> JSONGroupedObject:
if dest: if dest:
jsonObj.groups.append(dest) jsonObj.groups.append(dest)
return jsonObj return jsonObj if jsonObj.groups else None
## Flags ## Flags
@ -108,7 +108,7 @@ class HeaderFlag(IntFlag):
@staticmethod @staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self: def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: HeaderFlag = 0 flags: HeaderFlag = HeaderFlag(0)
flags |= { flags |= {
None: HeaderFlag.FORMAT_NONE, None: HeaderFlag.FORMAT_NONE,
@ -168,7 +168,7 @@ class ChecksumFlag(IntFlag):
@staticmethod @staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self: def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: ChecksumFlag = 0 flags: ChecksumFlag = ChecksumFlag(0)
flags |= { flags |= {
None: ChecksumFlag.CHECKSUM_WIDTH_NONE, None: ChecksumFlag.CHECKSUM_WIDTH_NONE,
@ -222,7 +222,7 @@ class IdentifierFlag(IntFlag):
@staticmethod @staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self: def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: IdentifierFlag = 0 flags: IdentifierFlag = IdentifierFlag(0)
flags |= { flags |= {
None: IdentifierFlag.PRIVATE_TID_TYPE_NONE, None: IdentifierFlag.PRIVATE_TID_TYPE_NONE,
@ -275,7 +275,7 @@ class SignatureFlag(IntFlag):
@staticmethod @staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self: def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: SignatureFlag = 0 flags: SignatureFlag = SignatureFlag(0)
flags |= { flags |= {
None: SignatureFlag.SIGNATURE_TYPE_NONE, 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): class GameFlag(IntFlag):
GAME_IO_BOARD_BITMASK = 7 << 0 GAME_INSTALL_RTC_HEADER_REQUIRED = 1 << 0
GAME_IO_BOARD_NONE = 0 << 0 GAME_RTC_HEADER_REQUIRED = 1 << 1
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
@staticmethod @staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self: def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: GameFlag = 0 flags: GameFlag = 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)]
for key, flag in { for key, flag in {
"installRequiresRTCHeader": GameFlag.GAME_INSTALL_RTC_HEADER_REQUIRED, "installRequiresRTCHeader": GameFlag.GAME_INSTALL_RTC_HEADER_REQUIRED,
@ -344,16 +387,6 @@ class GameFlag(IntFlag):
def toJSONObject(self) -> JSONGroupedObject: def toJSONObject(self) -> JSONGroupedObject:
return _makeJSONObject( 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": "installRequiresRTCHeader":
(GameFlag.GAME_INSTALL_RTC_HEADER_REQUIRED in self), (GameFlag.GAME_INSTALL_RTC_HEADER_REQUIRED in self),
"requiresRTCHeader": (GameFlag.GAME_RTC_HEADER_REQUIRED in self) "requiresRTCHeader": (GameFlag.GAME_RTC_HEADER_REQUIRED in self)
@ -362,11 +395,12 @@ class GameFlag(IntFlag):
## Data structures ## Data structures
_ROM_HEADER_INFO_STRUCT: Struct = Struct("< 4s 2s 3B x") ROM_HEADER_INFO_STRUCT: Struct = Struct("< 4s 2s 3B x")
_CART_INFO_STRUCT: Struct = Struct("< 8s 2s 6B") CART_INFO_STRUCT: Struct = Struct("< 8s 2s 6B")
_GAME_INFO_STRUCT: Struct = Struct("< 4s 36s 3s B 2H 10s 10s 16s 16s") GAME_INFO_STRUCT: Struct = Struct("< 4s 36s 4s 2H 4B 10s 10s 16s 16s")
_MAX_SPECIFICATIONS: int = 4
_MAX_REGIONS: int = 12 _MAX_SPECIFICATIONS: int = 4
_MAX_REGIONS: int = 12
@dataclass @dataclass
class ROMHeaderInfo: class ROMHeaderInfo:
@ -401,7 +435,7 @@ class ROMHeaderInfo:
) )
def toBinary(self) -> bytes: def toBinary(self) -> bytes:
return _ROM_HEADER_INFO_STRUCT.pack( return ROM_HEADER_INFO_STRUCT.pack(
self.signatureField, self.signatureField,
self.yearField, self.yearField,
self.headerFlags, self.headerFlags,
@ -454,7 +488,7 @@ class CartInfo:
) )
def toBinary(self) -> bytes: def toBinary(self) -> bytes:
return _CART_INFO_STRUCT.pack( return CART_INFO_STRUCT.pack(
self.dataKey, self.dataKey,
self.yearField, self.yearField,
self.pcb, self.pcb,
@ -476,7 +510,10 @@ class GameInfo:
series: str | None series: str | None
year: int year: int
flags: GameFlag ioBoard: IOBoardType
pcmcia1: PCMCIADeviceType
pcmcia2: PCMCIADeviceType
flags: GameFlag
bootloaderVersion: str | None = None bootloaderVersion: str | None = None
@ -502,7 +539,10 @@ class GameInfo:
obj.get("series", None), obj.get("series", None),
obj["year"], 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), obj.get("bootloaderVersion", None),
@ -524,7 +564,10 @@ class GameInfo:
"series": self.series, "series": self.series,
"year": self.year "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 "bootloaderVersion": self.bootloaderVersion
}, { }, {
@ -552,18 +595,21 @@ class GameInfo:
# FIXME: identifiers, series and bootloaderVersion are not currently # FIXME: identifiers, series and bootloaderVersion are not currently
# included in the binary format # included in the binary format
return _GAME_INFO_STRUCT.pack( return GAME_INFO_STRUCT.pack(
b"".join(sorted(ord(spec[1]) for spec in self.specifications)), bytes(sorted(ord(spec[1]) for spec in self.specifications)),
b"".join(sorted( b"".join(sorted(
region.encode("ascii").ljust(3, b"\0") region.encode("ascii").ljust(3, b"\0")
for region in self.regions for region in self.regions
)), )),
self.code.encode("ascii"), self.code.encode("ascii"),
self.flags,
nameOffset, nameOffset,
self.year, self.year,
self.rtcHeader .toBinary(), self.ioBoard,
self.flashHeader.toBinary(), self.pcmcia1,
self.installCart.toBinary(), self.pcmcia2,
self.gameCart .toBinary() 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. # characters (' ', '$', '%', '*') excluded.
_BASE41_CHARSET: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./:" _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: def toPrintableChar(value: int) -> str:
if (value < 0x20) or (value > 0x7e): if (value < 0x20) or (value > 0x7e):
@ -89,6 +91,9 @@ def decodeBase41(data: str) -> bytearray:
return output return output
def normalizeFileName(value: str) -> str:
return _NAME_INVALID_REGEX.sub("_", value)
def colorFromString(value: str) -> tuple[int, int, int]: def colorFromString(value: str) -> tuple[int, int, int]:
matched: re.Match | None = _COLOR_REGEX.match(value) matched: re.Match | None = _COLOR_REGEX.match(value)

View File

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