Fix reflashing, refactor, shorten cartdb region codes

This commit is contained in:
spicyjpeg 2023-06-25 09:56:47 +02:00
parent 51ac032198
commit 912ecb8d8d
No known key found for this signature in database
GPG Key ID: 5CC87404C01DF393
14 changed files with 181 additions and 119 deletions

View File

@ -34,10 +34,10 @@ add_executable(
src/uicommon.cpp
src/util.cpp
src/zs01.cpp
src/app/actions.cpp
src/app/app.cpp
src/app/cartactions.cpp
src/app/cartunlock.cpp
src/app/misc.cpp
src/app/unlock.cpp
src/libc/crt0.c
src/libc/cxxsupport.cpp
src/libc/malloc.c
@ -79,6 +79,7 @@ add_custom_command(
"${Python3_EXECUTABLE}"
"${PROJECT_SOURCE_DIR}/tools/convertExecutable.py"
-r "cart_tool build ${PROJECT_VERSION} - (C) 2022-2023 spicyjpeg"
-s 0x801dfff0
$<TARGET_FILE:cart_tool> cart_tool.psexe
BYPRODUCTS cart_tool.psexe
COMMENT "Converting executable"

Binary file not shown.

Binary file not shown.

View File

@ -253,17 +253,21 @@ void App::_cartReflashWorker(void) {
_dump.clearData();
pri->clear();
pri->cartID.copyFrom(_dump.cartID.data);
pri->updateTraceID(_reflashEntry->traceIDType, _reflashEntry->traceIDParam);
// The private installation ID seems to be unused on carts with a public
// data section.
if (pub) {
pri->installID.clear();
pub->setInstallID(_reflashEntry->installIDPrefix);
} else {
pri->setInstallID(_reflashEntry->installIDPrefix);
if (pri) {
if (_reflashEntry->flags & cart::DATA_HAS_CART_ID)
pri->cartID.copyFrom(_dump.cartID.data);
if (_reflashEntry->flags & cart::DATA_HAS_TRACE_ID)
pri->updateTraceID(
_reflashEntry->traceIDType, _reflashEntry->traceIDParam
);
if (_reflashEntry->flags & cart::DATA_HAS_INSTALL_ID) {
// The private installation ID seems to be unused on carts with a
// public data section.
if (pub)
pub->setInstallID(_reflashEntry->installIDPrefix);
else
pri->setInstallID(_reflashEntry->installIDPrefix);
}
}
_parser->setCode(_reflashEntry->code);

View File

@ -2,9 +2,9 @@
#pragma once
#include <stdint.h>
#include "app/actions.hpp"
#include "app/cartactions.hpp"
#include "app/misc.hpp"
#include "app/unlock.hpp"
#include "app/cartunlock.hpp"
#include "ps1/system.h"
#include "cart.hpp"
#include "cartdata.hpp"

View File

@ -1,5 +1,5 @@
#include "app/actions.hpp"
#include "app/cartactions.hpp"
#include "app/app.hpp"
#include "defs.hpp"
#include "uibase.hpp"

View File

@ -2,7 +2,7 @@
#include <stdio.h>
#include <string.h>
#include "app/app.hpp"
#include "app/unlock.hpp"
#include "app/cartunlock.hpp"
#include "cartdata.hpp"
#include "cartio.hpp"
#include "defs.hpp"

View File

@ -67,40 +67,45 @@ void IdentifierSet::updateTraceID(TraceIDType type, int param) {
uint8_t *input = &cartID.data[1];
uint16_t checksum = 0;
if (type == TID_81) {
// This format seems to be an arbitrary unique identifier not tied to
// anything in particular (perhaps RTC RAM?), ignored by the game.
traceID.data[0] = 0x81;
traceID.data[2] = 5;
traceID.data[5] = 7;
traceID.data[6] = 3;
switch (type) {
case TID_NONE:
return;
LOG("prefix=0x81");
goto _done;
case TID_81:
// This format seems to be an arbitrary unique identifier not tied
// to anything in particular (maybe RTC RAM?), ignored by the game.
traceID.data[0] = 0x81;
traceID.data[2] = 5;
traceID.data[5] = 7;
traceID.data[6] = 3;
LOG("prefix=0x81");
break;
case TID_82_BIG_ENDIAN:
case TID_82_LITTLE_ENDIAN:
for (size_t i = 0; i < ((sizeof(cartID.data) - 2) * 8); i += 8) {
uint8_t value = *(input++);
for (size_t j = i; j < (i + 8); j++, value >>= 1) {
if (value & 1)
checksum ^= 1 << (j % param);
}
}
traceID.data[0] = 0x82;
if (type == TID_82_BIG_ENDIAN) {
traceID.data[1] = checksum >> 8;
traceID.data[2] = checksum & 0xff;
} else {
traceID.data[1] = checksum & 0xff;
traceID.data[2] = checksum >> 8;
}
LOG("prefix=0x82, checksum=%04x", checksum);
break;
}
for (size_t i = 0; i < ((sizeof(cartID.data) - 2) * 8); i += 8) {
uint8_t value = *(input++);
for (size_t j = i; j < (i + 8); j++, value >>= 1) {
if (value & 1)
checksum ^= 1 << (j % param);
}
}
traceID.data[0] = 0x82;
if (type == TID_82_BIG_ENDIAN) {
traceID.data[1] = checksum >> 8;
traceID.data[2] = checksum & 0xff;
} else if (type == TID_82_LITTLE_ENDIAN) {
traceID.data[1] = checksum & 0xff;
traceID.data[2] = checksum >> 8;
}
LOG("prefix=0x82, checksum=%04x", checksum);
_done:
traceID.updateChecksum();
}

View File

@ -122,10 +122,7 @@ public:
if (diff)
return diff;
// If the provided region string is longer than this entry's region but
// the first few characters match, return 0. Do not however match
// strings shorter than this entry's region.
return __builtin_strncmp(region, _region, __builtin_strlen(region));
return __builtin_strncmp(region, _region, REGION_MAX_LENGTH);
}
inline int getDisplayName(char *output, size_t length) const {
return snprintf(output, length, "%s %s\t%s", code, region, name);

View File

@ -126,7 +126,6 @@ class IdentifierSet:
return TraceIDType.TID_81
case 0x82:
print(self.cartID,self.traceID)
checksum: int = self.getCartIDChecksum(param)
big: int = unpack("> H", self.traceID[1:3])[0]
little: int = unpack("< H", self.traceID[1:3])[0]
@ -141,6 +140,30 @@ class IdentifierSet:
case prefix:
raise ValueError(f"unknown trace ID prefix: 0x{prefix:02x}")
@dataclass
class PublicIdentifierSet:
installID: bytes | None = None # aka MID
systemID: bytes | None = None # aka XID
def __init__(self, data: bytes):
ids: list[bytes | None] = []
for offset in range(0, 16, 8):
_id: bytes = data[offset:offset + 8]
ids.append(_id if sum(_id) else None)
self.installID, self.systemID = ids
def getFlags(self) -> DataFlag:
flags: DataFlag = DataFlag(0)
if self.installID:
flags |= DataFlag.DATA_HAS_INSTALL_ID
if self.systemID:
flags |= DataFlag.DATA_HAS_SYSTEM_ID
return flags
## Cartridge dump structure
_DUMP_HEADER_STRUCT: Struct = Struct("< 2B 2x 8s 8s 8s 8s 8s")
@ -219,9 +242,10 @@ class ParserError(BaseException):
@dataclass
class Parser:
formatType: FormatType
flags: DataFlag
identifiers: IdentifierSet
formatType: FormatType
flags: DataFlag
identifiers: IdentifierSet
publicIdentifiers: PublicIdentifierSet
region: str | None = None
codePrefix: str | None = None
@ -236,13 +260,15 @@ class SimpleParser(Parser):
raise ParserError(f"invalid game region: {region}")
super().__init__(
FormatType.SIMPLE, flags, IdentifierSet(b""), region.decode("ascii")
FormatType.SIMPLE, flags, IdentifierSet(b""),
PublicIdentifierSet(b""), region.decode("ascii")
)
class BasicParser(Parser):
def __init__(self, dump: Dump, flags: DataFlag):
data: bytes = _getPublicData(dump, flags, _BASIC_HEADER_STRUCT.size)
ids: IdentifierSet = IdentifierSet(dump.data[_BASIC_HEADER_STRUCT.size:])
pri: IdentifierSet = IdentifierSet(dump.data[_BASIC_HEADER_STRUCT.size:])
region, codePrefix, checksum = _BASIC_HEADER_STRUCT.unpack(data)
@ -257,23 +283,29 @@ class BasicParser(Parser):
raise ParserError(f"invalid game region: {region}")
if bool(flags & DataFlag.DATA_HAS_CODE_PREFIX) != bool(codePrefix):
raise ParserError(f"game code prefix should{' not' if codePrefix else ''} be present")
if (ids.getFlags() ^ flags) & _IDENTIFIER_FLAG_MASK:
if (pri.getFlags() ^ flags) & _IDENTIFIER_FLAG_MASK:
raise ParserError("identifier flags do not match")
super().__init__(
FormatType.BASIC, flags, ids, region.decode("ascii"),
codePrefix.decode("ascii") or None
FormatType.BASIC, flags, pri, PublicIdentifierSet(b""),
region.decode("ascii"), codePrefix.decode("ascii") or None
)
class ExtendedParser(Parser):
def __init__(self, dump: Dump, flags: DataFlag):
data: bytes = _getPublicData(dump, flags, _EXTENDED_HEADER_STRUCT.size)
ids: IdentifierSet = IdentifierSet(dump.data[_EXTENDED_HEADER_STRUCT.size + 16:])
data: bytes = \
_getPublicData(dump, flags, _EXTENDED_HEADER_STRUCT.size + 16)
pri: IdentifierSet = \
IdentifierSet(dump.data[_EXTENDED_HEADER_STRUCT.size + 16:])
pub: PublicIdentifierSet = \
PublicIdentifierSet(data[_EXTENDED_HEADER_STRUCT.size:])
if flags & DataFlag.DATA_GX706_WORKAROUND:
data = data[0:1] + b"X" + data[2:]
code, year, region, checksum = _EXTENDED_HEADER_STRUCT.unpack(data)
code, year, region, checksum = \
_EXTENDED_HEADER_STRUCT.unpack(data[0:_EXTENDED_HEADER_STRUCT.size])
code: bytes = code.rstrip(b"\0")
region: bytes = region.rstrip(b"\0")
@ -287,13 +319,13 @@ class ExtendedParser(Parser):
raise ParserError(f"invalid game code: {code}")
if GAME_REGION_REGEX.fullmatch(region) is None:
raise ParserError(f"invalid game region: {region}")
if (ids.getFlags() ^ flags) & _IDENTIFIER_FLAG_MASK:
if (pri.getFlags() ^ flags) & _IDENTIFIER_FLAG_MASK:
raise ParserError("identifier flags do not match")
_code: str = code.decode("ascii")
super().__init__(
FormatType.EXTENDED, flags, ids, region.decode("ascii"), _code[0:2],
_code, year
FormatType.EXTENDED, flags, pri, pub, region.decode("ascii"),
_code[0:2], _code, year
)
## Cartridge database
@ -301,33 +333,11 @@ class ExtendedParser(Parser):
DB_ENTRY_STRUCT: Struct = Struct("< 6B H 8s 8s 8s 96s")
TRACE_ID_PARAMS: Sequence[int] = 16, 14
@dataclass
class GameEntry:
code: str
region: str
name: str
installCart: str | None = None
gameCart: str | None = None
ioBoard: str | None = None
# Implement the comparison overload so sorting will work.
def __lt__(self, entry: Any) -> bool:
return ( self.code, self.region, self.name ) < \
( entry.code, entry.region, entry.name )
def __str__(self) -> str:
return f"{self.code} {self.region}"
def getFullName(self) -> str:
return f"{self.name} [{self.code} {self.region}]"
def hasSystemID(self) -> bool:
return (self.ioBoard in SYSTEM_ID_IO_BOARDS)
@dataclass
class DBEntry:
game: GameEntry
code: str
region: str
name: str
dataKey: bytes
chipType: ChipType
@ -339,7 +349,9 @@ class DBEntry:
installIDPrefix: int = 0
year: int = 0
def __init__(self, game: GameEntry, dump: Dump, parser: Parser):
def __init__(
self, code: str, region: str, name: str, dump: Dump, parser: Parser
):
# Find the correct parameters for the trace ID heuristically.
_type: TraceIDType | None = None
@ -354,7 +366,9 @@ class DBEntry:
if _type is None:
raise RuntimeError("failed to determine trace ID parameters")
self.game = game
self.code = code
self.region = region
self.name = name
self.dataKey = dump.dataKey
self.chipType = dump.chipType
self.formatType = parser.formatType
@ -362,13 +376,17 @@ class DBEntry:
self.flags = parser.flags
self.year = parser.year or 0
if parser.identifiers.installID is None:
self.installIDPrefix = 0
else:
if parser.publicIdentifiers.installID is not None:
self.installIDPrefix = parser.publicIdentifiers.installID[0]
elif parser.identifiers.installID is not None:
self.installIDPrefix = parser.identifiers.installID[0]
else:
self.installIDPrefix = 0
# Implement the comparison overload so sorting will work.
def __lt__(self, entry: Any) -> bool:
return (self.game < entry.game)
return ( self.code, self.region, self.name ) < \
( entry.code, entry.region, entry.name )
def serialize(self) -> bytes:
return DB_ENTRY_STRUCT.pack(
@ -380,7 +398,7 @@ class DBEntry:
self.installIDPrefix,
self.year,
self.dataKey,
self.game.code.encode("ascii"),
self.game.region.encode("ascii"),
self.game.name.encode("ascii")
self.code.encode("ascii"),
self.region.encode("ascii"),
self.name.encode("ascii")
)

View File

@ -7,6 +7,7 @@ __author__ = "spicyjpeg"
import json, logging, os, re
from argparse import ArgumentParser, Namespace
from collections import Counter, defaultdict
from dataclasses import dataclass
from operator import methodcaller
from pathlib import Path
from struct import Struct
@ -16,6 +17,30 @@ from _common import *
## Game list (loaded from games.json)
@dataclass
class GameEntry:
code: str
region: str
name: str
installCart: str | None = None
gameCart: str | None = None
ioBoard: str | None = None
# Implement the comparison overload so sorting will work.
def __lt__(self, entry: Any) -> bool:
return ( self.code, self.region, self.name ) < \
( entry.code, entry.region, entry.name )
def __str__(self) -> str:
return f"{self.code} {self.region}"
def getFullName(self) -> str:
return f"{self.name} [{self.code} {self.region}]"
def hasSystemID(self) -> bool:
return (self.ioBoard in SYSTEM_ID_IO_BOARDS)
class GameDB:
def __init__(self, entries: Iterable[Mapping[str, Any]] | None = None):
self._entries: defaultdict[str, list[GameEntry]] = defaultdict(list)
@ -207,22 +232,28 @@ def processDump(
else:
parser.code = code.group().decode("ascii")
matches: list[GameEntry] = sorted(db.lookup(parser.code, parser.region))
matches: list[GameEntry] = sorted(db.lookup(parser.code, parser.region))
games: dict[str, DBEntry] = {}
if not matches:
raise RuntimeError(f"{parser.code} {parser.region} not found in game list")
names: str = ", ".join(map(methodcaller("getFullName"), matches))
logging.info(f"imported {dump.chipType.name}: {names}")
for game in matches:
if game.name in games:
continue
# TODO: handle separate installation/game carts
if game.hasSystemID():
parser.flags |= DataFlag.DATA_HAS_SYSTEM_ID
else:
parser.flags &= ~DataFlag.DATA_HAS_SYSTEM_ID
yield DBEntry(game, dump, parser)
games[game.name] = \
DBEntry(parser.code, parser.region, game.name, dump, parser)
logging.info(f"imported {dump.chipType.name}: {game.name}")
yield from games.values()
## Main

View File

@ -79,7 +79,7 @@ def parseDumpString(data: str) -> Dump:
def printDumpInfo(dump: Dump, output: TextIO):
if dump.flags & DumpFlag.DUMP_SYSTEM_ID_OK:
output.write(f"Digital I/O ID: {dump.systemID.hex('-')}\n")
output.write(f"Serial number: {serialNumberToString(dump.systemID)}\n")
output.write(f"Digital I/O SN: {serialNumberToString(dump.systemID)}\n\n")
output.write(f"Cartridge type: {_CHIP_NAMES[dump.chipType]}\n")
if dump.flags & DumpFlag.DUMP_CART_ID_OK:
@ -111,8 +111,8 @@ def createParser() -> ArgumentParser:
group = parser.add_argument_group("File paths")
group.add_argument(
"-i", "--input",
type = FileType("rt"),
help = "read dump from specified file",
type = FileType("rb"),
help = "read dump or QR string from specified file",
metavar = "file"
)
group.add_argument(
@ -122,12 +122,12 @@ def createParser() -> ArgumentParser:
help = "log cartridge info to specified file (stdout by default)",
metavar = "file"
)
#group.add_argument(
#"-e", "--export",
#type = FileType("wb"),
#help = "export dump in MAME format to specified file",
#metavar = "file"
#)
group.add_argument(
"-e", "--export",
type = FileType("wb"),
help = "export binary dump (.573 file) to specified path",
metavar = "file"
)
group.add_argument(
"data",
@ -144,16 +144,22 @@ def main():
if args.input:
with args.input as _file:
data: str = _file.read()
data: bytes = _file.read()
try:
dump: Dump = parseDump(data)
except:
dump: Dump = parseDumpString(data.decode("ascii"))
elif args.data:
data: str = args.data
dump: Dump = parseDumpString(args.data)
else:
parser.error("a dump must be passed on the command line or using -i")
dump: Dump = parseDumpString(data)
if args.log:
printDumpInfo(dump, args.log)
if args.export:
with args.export as _file:
_file.write(dump.serialize())
if __name__ == "__main__":
main()