#!/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 . # # 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 = ; 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< $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 < 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 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 <. $0 is available for download at . EOH ; exit; }