Created
August 26, 2025 12:12
-
-
Save chandujr/45916caa2fe12dad04cc0f4d62e2f01c to your computer and use it in GitHub Desktop.
Calculate RetroAchievements hash for PC Engine CD / TurboGrafx-CD games.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| pcecdhash.py - Calculate RetroAchievements hash for PC Engine CD / TurboGrafx-CD games. | |
| Make it executable if needed: | |
| chmod +x pcecdhash.py | |
| Usage: | |
| ./pcecdhash.py game.cue | |
| Requirements: | |
| - Python 3.x | |
| - A Redump-style cue/bin set (not trimmed/conversions) | |
| Authors: | |
| Chandu | |
| ChatGPT, 2025 | |
| Countless others | |
| """ | |
| import sys, os, hashlib | |
| SECTOR_SIZE = 2352 | |
| USERDATA_OFFSET = 16 | |
| USERDATA_SIZE = 2048 | |
| SECTORS_PER_SECOND = 75 | |
| def msf_to_sectors(msf: str) -> int: | |
| """Convert mm:ss:ff string to sector count.""" | |
| mm, ss, ff = [int(x) for x in msf.split(":")] | |
| return mm * 60 * SECTORS_PER_SECOND + ss * SECTORS_PER_SECOND + ff | |
| def read_sector(f, sector_index: int) -> bytes: | |
| """Read one sector of user data from a bin file.""" | |
| f.seek(sector_index * SECTOR_SIZE + USERDATA_OFFSET) | |
| return f.read(USERDATA_SIZE) | |
| def parse_cue_first_data_track(cue_path: str): | |
| """ | |
| Return (bin filename, INDEX 01 sector offset) for the first MODE1/MODE2 track. | |
| """ | |
| with open(cue_path, "r", encoding="utf-8", errors="ignore") as f: | |
| lines = f.readlines() | |
| current_file = None | |
| for i, line in enumerate(lines): | |
| parts = line.strip().split() | |
| if len(parts) >= 3 and parts[0].upper() == "FILE": | |
| filename = line.split('"')[1] if '"' in line else parts[1] | |
| current_file = os.path.join(os.path.dirname(cue_path), filename) | |
| if len(parts) >= 2 and parts[0].upper() == "TRACK": | |
| mode = parts[2].upper() | |
| if mode.startswith("MODE1") or mode.startswith("MODE2"): | |
| # find INDEX 01 for this track | |
| for subline in lines[i+1:]: | |
| subparts = subline.strip().split() | |
| if len(subparts) >= 3 and subparts[0].upper() == "INDEX" and subparts[1] == "01": | |
| return current_file, msf_to_sectors(subparts[2]) | |
| return None, 0 | |
| def compute_hash(cue_path: str) -> str: | |
| bin_path, start_sector = parse_cue_first_data_track(cue_path) | |
| if not bin_path or not os.path.exists(bin_path): | |
| raise FileNotFoundError("Could not locate data track from cue sheet.") | |
| with open(bin_path, "rb") as f: | |
| # Read 128 bytes from sector 1 relative to INDEX 01 | |
| header_sector_index = start_sector + 1 | |
| f.seek(header_sector_index * SECTOR_SIZE + USERDATA_OFFSET) | |
| header128 = f.read(128) | |
| # Verify magic string | |
| if b"PC Engine CD-ROM SYSTEM" not in header128[32:96]: | |
| raise ValueError("Invalid disc: Missing 'PC Engine CD-ROM SYSTEM' signature.") | |
| # Start building buffer | |
| buffer = bytearray(header128[-22:]) # last 22 bytes = disc title | |
| # Parse boot code info (big endian 3-byte index + 1-byte length) | |
| boot_index = (header128[0] << 16) | (header128[1] << 8) | header128[2] | |
| boot_sectors = header128[3] | |
| # Append boot code sectors | |
| for i in range(boot_sectors): | |
| buffer.extend(read_sector(f, start_sector + boot_index + i)) | |
| # MD5 hash | |
| return hashlib.md5(buffer).hexdigest() | |
| def main(): | |
| if len(sys.argv) != 2: | |
| print("Usage: pcecdhash.py game.cue") | |
| sys.exit(1) | |
| cue_path = sys.argv[1] | |
| if not os.path.exists(cue_path): | |
| print(f"Error: File not found: {cue_path}") | |
| sys.exit(1) | |
| try: | |
| digest = compute_hash(cue_path) | |
| print(f"RetroAchievements hash: {digest}") | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment