mirror of
https://github.com/spicyjpeg/573in1.git
synced 2025-02-02 04:27:56 +01:00
Fix reflashing, refactor, shorten cartdb region codes
This commit is contained in:
parent
51ac032198
commit
912ecb8d8d
@ -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.
BIN
data/zs01.cartdb
BIN
data/zs01.cartdb
Binary file not shown.
@ -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);
|
||||
|
@ -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"
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
#include "app/actions.hpp"
|
||||
#include "app/cartactions.hpp"
|
||||
#include "app/app.hpp"
|
||||
#include "defs.hpp"
|
||||
#include "uibase.hpp"
|
@ -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"
|
67
src/cart.cpp
67
src/cart.cpp
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
116
tools/_common.py
116
tools/_common.py
@ -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")
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user