Last active
April 30, 2023 19:06
-
-
Save dogtopus/92430a958c412d12d306241e671ad4a3 to your computer and use it in GitHub Desktop.
Keygen for the Japanese retail release of Frane: Dragons' Odyssey (2003) (aka: Lost Memory Of Angel Story FraneIII -緋蒼の幻想曲-)
This file contains 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
# Keygen for the Japanese retail release of Frane: Dragons' Odyssey (2003) | |
# For educational purpose only. I take no responsibility for what you will do with it. | |
# | |
# Also to EXE-Create: | |
# Quoting from GabeN: Piracy is a service issue. If FDO (2019) on Steam was actually half | |
# decent and wasn't having an allegedly dodgy localization and a lot of features from FDO (2003) | |
# removed (voice acting, mouse control and FMV cutscenes to name a few), this thing simply won't | |
# exist. | |
import argparse | |
import random | |
# Integer nibble to scrambled hex code and vice-versa | |
HEX_TABLE = {i: d for i, d in enumerate('9D8C53BF710246AE')} | |
HEX_TABLE_INVERSE = {d: i for i, d in HEX_TABLE.items()} | |
def parse_args(): | |
p = argparse.ArgumentParser() | |
sub = p.add_subparsers(dest='action') | |
action = sub.add_parser('generate', help='Generate a random serial number.') | |
action = sub.add_parser('generate-from-part1', help='Generate a serial number from a user-specified part 1.') | |
action.add_argument('part1', type=int, help='Part 1. Must be within range(30000, 50000).') | |
action.add_argument('-b', '--base', type=int, default=10, help='Override base for part 2 (default is 10). Must be within range(2, 16).') | |
action = sub.add_parser('validate', help='Validate a serial number.') | |
action.add_argument('sn', help='Serial number to validate.') | |
return p, p.parse_args() | |
# Shamelessly stolen from https://stackoverflow.com/questions/2267362/how-to-convert-an-integer-to-a-string-in-any-base | |
# because I'm too lazy to write my own | |
def number_to_base_le(n, b): | |
if n == 0: | |
return [0] | |
digits = [] | |
while n: | |
digits.append(int(n % b)) | |
n //= b | |
return digits | |
def base_to_num_le(digits, b): | |
n = 0 | |
for i, digit in enumerate(digits): | |
n += b ** i * digit | |
return n | |
def scramble_digit(digit): | |
return HEX_TABLE[digit] | |
def unscramble_digit(digit): | |
return HEX_TABLE_INVERSE.get(digit, 0) | |
def gen_sn_from_part1(part1, base): | |
if not (30000 <= part1 < 50000): | |
raise ValueError('Part1 must be within range(30000, 50000).') | |
if not (2 <= base < 16): | |
raise ValueError('Base must be within range(2, 16).') | |
part1_digits = number_to_base_le(part1, base) | |
checksum = (sum(part1_digits) // 3) % 100 | |
# checksum has fixed 2 digits of base 10 | |
checksum_digits = (checksum % 10, checksum // 10) | |
part2_base = scramble_digit(base) | |
part2_num = ''.join(scramble_digit(digit) for digit in part1_digits) | |
part2_checksum = ''.join(scramble_digit(digit) for digit in checksum_digits) | |
return f'{part1}-{part2_base}{part2_num}{part2_checksum}' | |
def gen_sn_random(): | |
part1 = random.randrange(30000, 50000) | |
base = random.randrange(2, 16) | |
return gen_sn_from_part1(part1, base) | |
# This doesn't emulate some edge cases and therefore isn't super accurate | |
def validate_sn(sn): | |
if len(sn) < 10: | |
raise ValueError('SN must be longer than 10 characters.') | |
parts = sn.split('-') | |
if len(parts) < 2: | |
raise ValueError('Part 2 does not exist.') | |
part1_str, part2_str = parts[0], '-'.join(parts[1:]) | |
# Original ignores error and parses as much as it can while this will error out and return nothing | |
part1 = int(part1_str, 10) | |
if len(part2_str) < 3: | |
# Original will wrap around and try to parse the other characters from the delimiter and part1 | |
# but we don't emulate that here | |
raise ValueError('Part 2 is too short.') | |
part2_base, part2_num, part2_checksum = part2_str[0], part2_str[1:-2], part2_str[-2:] | |
part2_base_int = unscramble_digit(part2_base) | |
part2_digits = tuple(unscramble_digit(digit) for digit in part2_num) | |
part2_checksum_expected = base_to_num_le(tuple(unscramble_digit(digit) for digit in part2_checksum), 10) | |
part2_checksum_actual = (sum(part2_digits) // 3) % 100 | |
if part2_checksum_actual != part2_checksum_expected: | |
raise ValueError('Invalid part 2 checksum.') | |
part2 = base_to_num_le(part2_digits, part2_base_int) | |
if not (30000 <= part1 < 50000): | |
raise ValueError('Part 1 must be in range (30000, 50000).') | |
if part1 != part2: | |
raise ValueError('Parts do not match.') | |
if __name__ == '__main__': | |
p, args = parse_args() | |
if args.action == 'generate-from-part1': | |
print(gen_sn_from_part1(args.part1, args.base)) | |
elif args.action == 'validate': | |
try: | |
validate_sn(args.sn) | |
except ValueError as e: | |
print(f'{args.sn} is NOT a valid serial number: {e}') | |
else: | |
print(f'{args.sn} is a valid serial number.') | |
else: | |
print(gen_sn_random()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Also as a bonus: a cringy but funny engineered S/N:
37448-5-BFF-R1el-Kunah-19
.