#!/usr/bin/perl
#
#	gg - convert a Game Genie code for the N.E.S.
#	Col. G. L. Sicherman (colonel@monmouth.com).  2002-07-30.
#
#	gg <code>
#	gg <address> <value> [checkvalue]
#
#	If you specify a code, prints the address, value, and comparison
#	value if any.  Case is ignored in the argument.  All output values
#	are in hexadecimal.
#
#	If you specify an address, a value, and an optional comparison
#	value, prints the code.  All arguments are in hexadecimal;
#	case is ignored. (So is the high bit of the address.)
#
#	This follows the mapping defined in Benzene's document 0.71.
#	The algorithm is better than his, though!

sub usage { die "usage:\tgg <code>\n\tgg <address> <value> [checkvalue]\n" }

$ggchar = "AEPOZXLUGKISTVYN";

if (@ARGV==1) {
	$code = shift;
	&decode;			# No return.
}
if (@ARGV==2 || @ARGV==3) {
	($addr, $value, $checkvalue) = @ARGV;
	&encode;			# No return.
}
&usage;

sub decode {
	$codelen = length $code;
	if ($codelen != 6 && $codelen != 8) {
		die "gg: code must have 6 or 8 characters\n";
	}
	$code = uc $code;
	for (split //, $code) {
		$i = index $ggchar, $_;
		$i >= 0
		or die("gg: illegal code character " . &showchar($_) . "\n");
		$bits = ($bits << 4) | $i;
	}

	# Rotate the bits 1 position rightward.

	$tailbit = $bits & 1;
	$bits = ($bits >> 1) | ($tailbit << ($codelen==8? 31: 23));
	$bits <<= 8 if $codelen==6;	# Pad out with zero bits.
	$iscond = ($bits >> 19) & 1;
	if ($iscond && $codelen==6) {
		die "gg: high bit not set for conditional code\n";
	}
	if (!$iscond && $codelen==8) {
		die "gg: high bit set for unconditional code\n";
	}
	$addr = (($bits & 0x000f0000) >> 4) | (($bits & 0x00000f00) >> 0)
		| (($bits & 0x00f00000) >> 16) | (($bits & 0x0000f000) >> 12)
		| 0x8000;
	$value = (($bits & 0x0f000000) >> 20) | (($bits & 0xf0000000) >> 28);
	printf "%04x %02x", $addr, $value;
	if ($codelen==8) {
		$checkvalue = (($bits & 0x0000000f) << 4)
			| (($bits & 0x000000f0) >> 4);
		printf " %02x", $checkvalue;
	}
	print "\n";
	exit 0;
}

sub showchar {
	$ord = ord $_[0];
	return $ord if 0x41 <= $ord && $ord <= 0x7e;
	return "SP" if 0x40 == $ord;
	return "^?" if 0x7f == $ord;
	return "^" . chr(0x40 + $ord) if $ord < 0x40;
	return "M-" . &showchar(chr($ord) & 0x7f);
}

sub encode {
	$addr =~ /^[0-9a-f]{1,4}$/i or &usage;
	$zaddr = hex $addr;
	$value =~ /^[0-9a-f]{1,2}$/i or &usage;
	$zvalue = hex $value;
	if (@ARGV==3) {
		$checkvalue =~ /^[0-9a-f]{1,2}$/i or &usage;
		$zcheckvalue = hex $checkvalue;
		$zaddr |= 0x8000;
	}
	else {
		$zcheckvalue = 0;
		$zaddr &= ~0x8000;
	}
	$bits = $zvalue & 0xf;
	$bits <<= 4;
	$bits |= ($zvalue & 0xf0) >> 4;
	$bits <<= 4;
	$bits |= ($zaddr & 0x00f0) >> 4;
	$bits <<= 4;
	$bits |= ($zaddr & 0xf000) >> 12;
	$bits <<= 4;
	$bits |= $zaddr & 0x000f;
	$bits <<= 4;
	$bits |= ($zaddr & 0x0f00) >> 8;
	$bits <<= 4;
	$bits |= $zcheckvalue & 0x0f;
	$bits <<= 4;
	$bits |= ($zcheckvalue & 0xf0) >> 4;

#	Rotate the bits 1 position left.

	$headbit = ($bits >> 31) & 1;
	$bits = ($bits << 1) | $headbit;
	for ($pos=7; $pos>=0; $pos--) {
		$code = substr($ggchar, $bits & 0x0000000f, 1) . $code;
		$bits >>= 4;
	}
	$code = substr $code, 0, 6 if @ARGV==2;
	print $code, "\n";
	exit 0;
}
