#!/usr/bin/perl
#
# civreplay
#   Copyright (c) 2001,2002 by Michael A. Gurski.  All Rights Reserved.
#
# This script is distributed under the terms of the GNU General Public
# License, available at <http://www.gnu.org/copyleft/gpl.html>.
#
#
use warnings;
use Compress::Zlib;  # needed for gzipped savegames
use Getopt::Long;    # needed to process command-line options
use File::Basename;  # needed for grabbing basenames of files

# default values if no options are passed in
my $delay = 5; # in 100ths of a second
my $outputName = "civreplay.gif";
my $pixelHeight = 2;
my $pixelWidth = 2;
my $radius = 2;
my $showUnits = 0;

my $VERSION = "0.003";

# display help if nothing passed in on command line
&help if($#ARGV == -1);

# options for the script, along with alternate versions
GetOptions(
	   'delay|d=i' => \$delay,
	   'expansion|e=i' => \$expansion,
	   'output|o=s' => \$outputName,
	   'pixelheight|h|height=i' => \$pixelHeight,
	   'pixelwidth|w|width=i' => \$pixelWidth,
	   'quiet|q+' => \$quiet,
	   'radius|r=i' => \$radius,
	   'units|u+' => \$showUnits,
	   'version|V+' => \$showVersion,
	   );

&showVersion if($showVersion);

# set pixelWidth and pixelHeight if expansion has been given on command-line
if(defined($expansion)) {
    $pixelWidth = $expansion;
    $pixelHeight = $expansion;
}

# colors, in RRR GGG BBB format, used for creating the PPM files
my $colors = 255;
my $seacolor  = "  0   0 255  ";
my $landcolor = "234 234 234  ";
my @playercolors =
    (
     "  0 255   0  ", # player 0
     "  0 255 255  ", # player 1
     "255   0   0  ", # player 2
     "255   0 255  ", # player 3
     "255 255   0  ", # player 4
     "108 123 139  ", # player 5
     "144 238 144  ", # player 6
     "148   0 211  ", # player 7
     "152 251 152  ", # player 8
     "173 216 230  ", # player 9
     "184 134  11  ", # player 10
     "193 205 205  ", # player 11
     "233 150 122  ", # player 12
     "240 255 240  ", # player 13
     "255 105 180  ", # player 14
     "255 165   0  ", # player 15
     "255 218 185  ", # player 16
     "118 238 198  ", # player 17
     "238 201   0  ", # player 18
     "139 115  85  ", # player 19
     "128   0   0  ", # player 20
     "  0 128   0  ", # player 21
     "128 128   0  ", # player 22
     "128   0 128  ", # player 23
     "  0 128 128  ", # player 24
     " 96   0   0  ", # player 25
     "  0  96   0  ", # player 26
     " 96  96   0  ", # player 27
     " 96   0  96  ", # player 28
     "  0  96  96  ", # player 29
     );

my $barbariancolor = " 64  64  64  ";

# try to get an inflation stream for reading compressed files, die if we can't
my $input = inflateInit() || die "Cannot create an inflation stream\n";

my $counter = 0;  # counter to keep track of how many maps we're creating

# loop through savegame files, sorting based on the year in the file's name
foreach my $i (sort byYear @ARGV) {
    my $buffer = '';
    my $mbuff = '';

    # if it's compressed, decompress it
    if($i =~ /\.gz$/) {
	my $gz = gzopen($i, "rb");
	
	$mbuff .= $buffer while $gz->gzread($buffer) >0;
	
	die "Error reading from $i: $gzerrno" . ($gzerrno+0) . "\n"
	    if $gzerrno != Z_STREAM_END;
	
	$gz->gzclose();
    }
    # otherwise just read it in
    else {
	my $old_rs = $/;
	undef $/;

	open(FILE,"$i");
	$mbuff = <FILE>;
	close(FILE);

	$/ = $old_rs;
    }

    # parse the save
    print STDERR "reading config - $counter\n" unless($quiet);
    %config = &readIniFile($mbuff);
    # create in-memory array for map
    print STDERR "creating map - $counter\n" unless($quiet);
    &createMap;
    # fill the players' cities in on the map
    print STDERR "filling cities - $counter\n" unless($quiet);
    &fillCities;
    # fill the players' cities in on the map
    print STDERR "filling units - $counter\n" if($showUnits && !$quiet);
    &fillUnits if($showUnits);
    # write out the map
    print STDERR "writing map - $counter\n" unless($quiet);
    &writeMap;

    $counter++;
}

# animate all the maps
print STDERR "animating...\n" unless($quiet);
system("gifsicle -O2 --colors 33 --careful -w --delay $delay /tmp/civreplay_${$}_*.gif > $outputName");
system("/bin/rm /tmp/civreplay_${$}_*.gif");

# helper function for sort, recognizes how to properly compare files of
# type "blahblahblah[-]year.sav[.gz]", so that we don't end up with -3950
# coming after 2000...
sub byYear {
    my $aa = File::Basename::basename($a);
    my $bb = File::Basename::basename($b);

    $aa =~ s/^[^-\d]*(-?\d+)\.sav(\.gz)?/$1/;
    $bb =~ s/^[^-\d]*(-?\d+)\.sav(\.gz)?/$1/;

    return $aa <=> $bb;
}

# read in the savegame file from the buffer passed in. it looks remarkably like
# a windows .ini file, with a few twists
sub readIniFile {
    my $buffer = shift;

    # break buffer into lines
    my @buffer = split('\n', $buffer);

    my %config;
    my $sect;

    # loop through the buffer
    while(@buffer) {
	my $line = shift @buffer;

	# sections begin with [sectionname]
	if($line =~ /^\[(.+)\]\s*$/) {
	    $sect = $1;
	}
	# the twist: multi-line values in the file are written like:
	#  name={ val1,
	#  val2,
	#  ...
	#  valN,
	#  }
	elsif($line =~ /^([^=]*)=\{\s(.*)/) {
	    # save first value
	    push(@{$config{$sect}{$1}},$2);
	    my $item = $1;

	    # loop through remaining values
	    $line = shift @buffer;
	    while($line !~ /^\}/) {
		push(@{$config{$sect}{$item}},$line);
		$line = shift @buffer;
	    }
	}
	# normal name=value pair
	elsif($line =~ /^([^=]*)=(.*)/) {
	    $config{$sect}{$1} = $2;
	}
	# either a blank line, or something else we don't know/care about
	else {
	    next;
	}
    }

    # the new config!
    return %config;
}

# create the map in-memory from the config file
sub createMap {

    # maps are stored line-by-line in the config
    for(my $i = 0; $i < $config{map}{height}; $i++) {
	# from what i've gathered, the tNNN lines in [map] are the master map
	my $num = sprintf "t%03d",$i;
	my $line = $config{map}{$num};
	$line =~ s/\"//g;	# get rid of the quotes around the map line

	$line =~ s/\S/./g;	# since we don't care what actual land squares
				# are, convert them all to "."

	# split the line into an array, so we have an easier time accessing it
	# later
	my @elem = split(/(?=.)/, $line);
	$mapData{$i} = \@elem;
    }
}

# write out the map in PPM format, then convert to a .gif
sub writeMap {
    # width and height in pixels -- a function of the map width and height,
    # as well as the pixelWidth and pixelHeight values passed in
    my $width = $config{map}{width} * $pixelWidth;
    my $height = $config{map}{height} * $pixelHeight;

    # temp name of the ppm output file
    my $ppmname = sprintf("/tmp/civreplay_${$}_%09d.ppm", $counter);
    # temp name of the individual gif file
    my $gifname = sprintf("/tmp/civreplay_${$}_%09d.gif", $counter);

    open(OUTMAP, ">$ppmname");

    # the ppm header information
    print OUTMAP<<EOH
P3
# map.ppm
$width $height
$colors
EOH
    ;

    # loop through the map data, printing out the bitmap information, line by
    # line
    for(my $i = 0; $i < $config{map}{height}; $i++) {
	# do each line $pixelHeight times
	for(my $j = 0; $j < $pixelHeight; $j++) {
	    # loop through individual elements on the current line
	    foreach my $k (@{$mapData{$i}}) {
		# print each element $pixelWidth times
		for(my $l = 0; $l < $pixelWidth; $l++) {
		    # a sea square
		    if($k eq " ") {
			print OUTMAP "$seacolor";
		    }
		    # an unoccupied land square
		    elsif($k eq ".") {
			print OUTMAP "$landcolor";
		    }
		    elsif($k eq "barb") {
			print OUTMAP "$barbariancolor";
		    }
		    # a controlled land square
		    else {
			print OUTMAP "$playercolors[$k]";
		    }
		}
	    }
	}
	# end each line of image with a newline
	print OUTMAP "\n";
    }

#      # i don't like this, but it was the best way i could think of to force
#      # each gif file to have the same colormap. it prints a line at the bottom
#      # of the image, containing every player color in a separate pixel
#      for(my $i = 0; $i < $width; $i++) {
#  	my $q = $i % ($#playercolors + 1);
#  	print OUTMAP "$playercolors[$q]\n";
#      }

    close(OUTMAP);

    # convert the ppm to a gif
    system("ppmtogif -comment \"Created with civreplay $VERSION. Frame $counter.\" -sort $ppmname > $gifname 2>/dev/null");
    # delete the ppm
    system("/bin/rm","$ppmname");
}


# place the players cities on the map, filling in areas of influence
sub fillCities {
    my @players;
    # probably not strictly necessary, but sets up @players to contain the
    # player numbers
    for(my $i = 0; $i < $config{game}{nplayers}; $i++) {
	$players[$i] = "$i";

	# special case for barbarians
	$players[$i] = "barb" if($config{"player$i"}{race} == 61);
    }

    # loop through each player in the game at this point
    for(my $i = 0; $i < $config{game}{nplayers}; $i++) {
	my $player = "player$i";

	# if they have no cities (ie, dead), skip them
	next if(!defined(@{$config{$player}{c}}));

	# get the list of cities the player owns
	my @cities = @{$config{$player}{c}};
	shift @cities; # get rid of explanation line

	# loop through each city
	foreach my $line (@cities) {
	    my @cityinfo = split(',',$line);

	    # get the coordinates of the city
	    my $x = $cityinfo[1];
	    my $y = $cityinfo[2];

	    # modify the surrounding squares of the map
	    for(my $j=$y - $radius; $j <= $y + $radius; $j++) {
		# the world is a cylinder, so if the radius causes us to go
		# too far north/south, just continue on
		next if($j < 0);
		next if($j >= $config{map}{height});

		for(my $k=$x - $radius; $k <= $x + $radius; $k++) {
		    my $xx = $k;
		    # the world is a cylinder, so we wrap around if the X
		    # coordinates happen to go too far east/west
		    $xx += $config{map}{width} if($xx < 0);
		    $xx -= $config{map}{width} if($xx >= $config{map}{width});

		    # only set the square's color if not already defined,
		    # or is the actual city location (in cases of a lone city
		    # completely surrounded by the enemy
		    ${$mapData{$j}}[$xx] = $players[$i]
			if(${$mapData{$j}}[$xx] eq "." ||
			    ($j == $y && $k == $x));
		}
	    }
	}
    }
}

# place the players units on the map
sub fillUnits {
    my @players;
    # probably not strictly necessary, but sets up @players to contain the
    # player numbers
    for(my $i = 0; $i < $config{game}{nplayers}; $i++) {
	$players[$i] = "$i";

	# special case for barbarians
	$players[$i] = "barb" if($config{"player$i"}{race} == 61);
    }

    # units don't have a radius, imho
    my $old_radius = $radius;
    $radius = 0;

    # loop through each player in the game at this point
    for(my $i = 0; $i < $config{game}{nplayers}; $i++) {
	my $player = "player$i";

	# if they have no units (ie, dead), skip them
	next if(!defined(@{$config{$player}{u}}));

	# get the list of units the player owns
	my @units = @{$config{$player}{u}};
	shift @units; # get rid of explanation line

	# loop through each unit
	foreach my $line (@units) {
	    my @unitinfo = split(',',$line);

	    # get the coordinates of the unit
	    my $x = $unitinfo[1];
	    my $y = $unitinfo[2];

	    # modify the surrounding squares of the map
	    for(my $j=$y - int($radius/2); $j <= $y + int($radius/2); $j++) {
		# the world is a cylinder, so if the radius causes us to go
		# too far north/south, just continue on
		next if($j < 0);
		next if($j >= $config{map}{height});

		for(my $k=$x - int($radius/2); $k <= $x + int($radius/2); $k++) {
		    my $xx = $k;
		    # the world is a cylinder, so we wrap around if the X
		    # coordinates happen to go too far east/west
		    $xx += $config{map}{width} if($xx < 0);
		    $xx -= $config{map}{width} if($xx >= $config{map}{width});

		    # only set the square's color if not already defined,
		    # or is the actual unit location (in cases of a lone city
		    # completely surrounded by the enemy
		    ${$mapData{$j}}[$xx] = $players[$i];
#			if(${$mapData{$j}}[$xx] eq "." ||
#			    ($j == $y && $k == $x));
		}
	    }
	}
    }

    # set radius back for later iterations
    $radius = $old_radius;
}

# useful help information
sub help {
    print <<EOH;

Proper usage:
  $0 [options] <savegamefiles*.sav[.gz]>

where options are:
 --delay|-d N           delay between gif frames in 100ths of a second
                          (default $delay)
 --expansion|-e N       expansion factor for pixels (sets both pixelwidth
                          and pixelheight to the same value)
 --output|-o <filename> the output filename for the animated gif
                          (default $outputName)
 --pixelheight|--height|-h N
                        the height in pixels of individual map squares
                          (default $pixelHeight)
 --pixelwidth|--width|-w N
                        the width in pixels of individual map squares
                          (default $pixelWidth)
 --quiet|-q             run quietly
 --radius|-r N          how many map squares around a city should be colored
                          a player's particular color.  A radius of 0
                          means that only cities themselves are noted.  A
                          radius of 2 roughly indicates the squares a player's
                          cities actually control.
                          (default $radius)
 --units|-u             display units as well as cities. Units always have a
                           radius of 0 (default off)
 --version|-v           display the version information

EOH
    ;

    exit;
}

sub showVersion {
    print <<EOH;

$0 version $VERSION.

Distributed under the terms of the GNU General Public License, which
is available at <http://www.gnu.org/copyleft/gpl.html>.

$0 is available for download at
<http://www.pobox.com/~mgurski00001/software/civreplay/>.

EOH
    ;

    exit;
}
