mirror of
https://github.com/spicyjpeg/573in1.git
synced 2025-03-01 07:20:42 +01:00
Fix errors, add gamedb builder and PCMCIA card info
This commit is contained in:
parent
813f939bde
commit
4b57169e64
File diff suppressed because it is too large
Load Diff
1163
data/games.json
1163
data/games.json
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) ||
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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")
|
||||
|
@ -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""
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user