import struct from construct import Struct, Int16ul, Int32ul, PaddedString from enum import Enum import re from typing import Union, Final class LogStatus(Enum): NONE = 0 START = 1 CONTINUE = 2 END = 3 OTHER = 4 class PortalRegStatus(Enum): NO_REG = 0 PORTAL = 1 SEGA_ID = 2 class ADBStatus(Enum): UNKNOWN = 0 GOOD = 1 BAD_AMIE_ID = 2 ALREADY_REG = 3 BAN_SYS_USER = 4 BAN_SYS = 5 BAN_USER = 6 BAN_GEN = 7 LOCK_SYS_USER = 8 LOCK_SYS = 9 LOCK_USER = 10 class CompanyCodes(Enum): NONE = 0 SEGA = 1 BAMCO = 2 KONAMI = 3 TAITO = 4 class ReaderFwVer(Enum): # Newer readers use a singly byte value NONE = 0 TN32_10 = 1 TN32_12 = 2 OTHER = 9 def __str__(self) -> str: if self == self.TN32_10: return "TN32MSEC003S F/W Ver1.0" elif self == self.TN32_12: return "TN32MSEC003S F/W Ver1.2" elif self == self.NONE: return "Not Specified" elif self == self.OTHER: return "Unknown/Other" else: raise ValueError(f"Bad ReaderFwVer value {self.value}") @classmethod def from_byte(self, byte: bytes) -> Union["ReaderFwVer", int]: try: i = int.from_bytes(byte, 'little') try: return ReaderFwVer(i) except ValueError: return i except TypeError: return 0 class ADBHeaderException(Exception): pass HEADER_SIZE: Final[int] = 0x20 CMD_CODE_GOODBYE: Final[int] = 0x66 # everything is LE class ADBHeader: def __init__(self, magic: int, protocol_ver: int, cmd: int, length: int, status: int, game_id: Union[str, bytes], store_id: int, keychip_id: Union[str, bytes]) -> None: self.magic = magic # u16 self.protocol_ver = protocol_ver # u16 self.cmd = cmd # u16 self.length = length # u16 try: self.status = ADBStatus(status) # u16 except ValueError as e: raise ADBHeaderException(f"Status is incorrect! {e}") self.game_id = game_id # 4 char + \x00 self.store_id = store_id # u32 self.keychip_id = keychip_id# 11 char + \x00 if type(self.game_id) == bytes: self.game_id = self.game_id.decode() if type(self.keychip_id) == bytes: self.keychip_id = self.keychip_id.decode() self.game_id = self.game_id.replace("\0", "") self.keychip_id = self.keychip_id.replace("\0", "") if self.cmd != CMD_CODE_GOODBYE: # Games for some reason send no data with goodbye self.validate() @classmethod def from_data(cls, data: bytes) -> "ADBHeader": magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data) head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id) if head.length != len(data): raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}") return head def validate(self) -> bool: if self.magic != 0xa13e: raise ADBHeaderException(f"Magic {self.magic} != 0xa13e") if self.protocol_ver < 0x1000: raise ADBHeaderException(f"Protocol version {hex(self.protocol_ver)} is invalid!") if re.fullmatch(r"^S[0-9A-Z]{3}[P]?$", self.game_id) is None: raise ADBHeaderException(f"Game ID {self.game_id} is invalid!") if self.store_id == 0: raise ADBHeaderException(f"Store ID cannot be 0!") if re.fullmatch(r"^A[0-9]{2}[E|X][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None: raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!") return True def make(self) -> bytes: resp_struct = Struct( "magic" / Int16ul, "unknown" / Int16ul, "response_code" / Int16ul, "length" / Int16ul, "status" / Int16ul, "game_id" / PaddedString(6, 'utf_8'), "store_id" / Int32ul, "keychip_id" / PaddedString(12, 'utf_8'), ) return resp_struct.build(dict( magic=self.magic, unknown=self.protocol_ver, response_code=self.cmd, length=self.length, status=self.status.value, game_id = self.game_id, store_id = self.store_id, keychip_id = self.keychip_id, )) class ADBBaseRequest: def __init__(self, data: bytes) -> None: self.head = ADBHeader.from_data(data) class ADBBaseResponse: def __init__(self, code: int = 0, length: int = 0x20, status: int = 1, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 0x3087) -> None: self.head = ADBHeader(0xa13e, protocol_ver, code, length, status, game_id, store_id, keychip_id) @classmethod def from_req(cls, req: ADBHeader, cmd: int, length: int = 0x20, status: int = 1) -> "ADBBaseResponse": return cls(cmd, length, status, req.game_id, req.store_id, req.keychip_id, req.protocol_ver) def append_padding(self, data: bytes): """Appends 0s to the end of the data until it's at the correct size""" padding_size = self.head.length - len(data) data += bytes(padding_size) return data def make(self) -> bytes: return self.head.make()