use warnings;
use Data::Dumper;
+our $VERSION = '1.00';
+
my $SHOWWARN = 0;
+my $APMSVG = undef;
-use Getopt::Long;
+use Getopt::Long qw(:config bundling auto_version auto_help);
GetOptions(
"verbose|v!" => \$SHOWWARN,
+ "apm|a=s" => \$APMSVG,
);
+use constant { APM_FIRSTFRAME => 80 / .042 };
+
{
package Data::StarCraft::Replay;
0x18 => ["cancel"],
0x19 => ["cancel hatch"],
0x1A => ["stop", 1],
+# 0x1B => ["move-thing??"], # tim: after hotkey (unit, reaver??) select; soon after reselected and moved
0x1E => ["return cargo", 1],
0x1F => ["train", 2, \%unit],
0x20 => ["cancel train", 2], # == 254
}
+my @race = (qw(Z T P), (undef) x 3, '-');
+
sub showtime {
my $time = shift() * .042;
my $minutes = int($time / 60);
}
local $_ = Data::StarCraft::Replay::_read(undef, \*STDIN, 633)
- and my ($head, @headdata) = unpackhash("CVa3Va12Z28v2Z16Z24Ca26a38a*", $_, qw(
+ and my ($head, @headdata) = unpackhash("CVa3Va12Z28v2Z16Z24CZ26a38a*", $_, qw(
engine frames mag1 time mag2 name width height
unknown1 creator unknown2 map unknown3
))
) for $head->{mag2};
delete $head->{$_} for qw(mag1 mag2 unknown1 unknown2);
-my @playdata = unpack "a36"x12 . "V8C8", $headdata[0]
+my @playdata = unpack "Va32"x12 . "V8C8", $headdata[0]
or die "Couldn't parse player data in replay header\n";
-my @player;
-push @player, unpackhash("x11Z25", shift @playdata, qw/name/) for 0 .. 11;
+my (@player, @slot);
+for (0 .. 11) {
+ my $number = shift @playdata;
+ defined $player[$number] and warn "Player #$number redefined";
+ my ($data) = unpackhash("VcccZ25", shift @playdata, qw(
+ slot type race team name
+ ));
+ defined $race[$_] ? ($data->{race} = $race[$_]) :
+ warn "Unknown race #$_ for player $number"
+ for $data->{race};
+ $slot[$data->{slot}] = $number if $data->{slot} < 16;
+ $player[$number] = $data;
+}
$player[$_]->{color} = shift @playdata for 0 .. 7;
$player[$_]->{index} = shift @playdata for 0 .. 7;
+sub showplayer {
+ my $id = shift;
+ my $playdata = $player[$slot[$id]];
+ return defined $playdata ?
+ sprintf '%s (%s)', $playdata->{name}, $playdata->{race} : "#$id";
+}
+
printf "%s: %s\n", $_, $head->{$_} for qw(name creator);
use Date::Format;
printf "created: %s\n", time2str('%Y-%m-%d %X', $_) for $head->{time};
my %stats; # player => count
for (@$map) {
$stats{$_->[1]}{actions}++;
- $stats{$_->[1]}{gameactions}++ if $_->[0] > 80 / .042;
+ $stats{$_->[1]}{gameactions}++ if $_->[0] >= APM_FIRSTFRAME;
$stats{$_->[1]}{last} = $_->[0] if $_->[2] eq "part";
$stats{$_->[1]}{$cmdmacro{$_->[2]} ? "macro" : "micro"}++;
$stats{$_->[1]}{count}{$_->[2]}++;
for my $player (sort keys %stats) {
my $row = $stats{$player};
$row->{last} ||= $map->[-1][0];
-# printf("%d:%6d actions (%3d micro,%4d macro);%4d APM\n",
- printf("%d:%6d actions in%7d frames (%s) = %d APM\n",
- $player, $row->{actions}, $row->{last},
+# printf("%-16s%6d actions (%3d micro,%4d macro);%4d APM\n",
+ my $name = showplayer($player);
+ printf("%d %-16s%6d actions in%7d frames (%s) = %d APM\n",
+ $row->{slot},
+ $name, $row->{actions}, $row->{last},
showtime($row->{last}),
# $row->{micro} / $row->{last} * 60 / .042 * 1.05,
# $row->{macro} / $row->{last} * 60 / .042 * 1.05,
- $row->{gameactions} / $row->{last} * 60 / .042 * 1.042,
- # $row->{gameactions} / $map->[-1][0] * 60 / .042,
+ $row->{gameactions} / ($row->{last} - APM_FIRSTFRAME) * 60 / .042,
);
if (0) {
) if 0;
}
+if ($APMSVG) {
+ my @seq; # player => time (s) => actions
+ $seq[$_->[1]][$_->[0] * .042]++ for @$map;
+ my $flatten = 120;
+ my @apm;
+ for my $player (0 .. $#seq) {
+ my $range = 0;
+ $range += $seq[$player][$_] || 0 for 0 .. $flatten - 1;
+ my $leadfill = $range / $flatten;
+ for my $frame (0 .. $#{$seq[$player]}) {
+ $range += $seq[$player][$frame] || 0;
+ $range -= $frame < $flatten ? $leadfill :
+ $seq[$player][$frame - $flatten] || 0;
+ $apm[$player][$frame] = $range / $flatten;
+ }
+ }
+
+ BEGIN { unshift @INC, '.' }
+ use SVG::TT::Graph::TimeSeries;
+ my $graph = SVG::TT::Graph::TimeSeries->new({
+ height => 1200,
+ width => 1600,
+ style_sheet => "apm.css",
+ show_data_values => 0,
+ show_data_points => 0,
+ x_label_format => '%k:%M',
+ key => 1,
+ timescale_divisions => "5 minutes",
+ # compress => 1,
+ });
+
+ for my $player (0 .. $#apm) {
+ $graph->add_data({
+ data => [map {
+ time2str('%Y-%m-%d %X', 946681200 + $_),
+ $apm[$player][$_] * 60
+ } 0 .. $#{$apm[$player]} ],
+ title => showplayer($player),
+ });
+ }
+
+ my ($name) = $APMSVG =~ /([^.]+)/;
+ my $title = "APM timeline" . ($name && " for $name");
+ my $lead = sprintf "\n<title>%s</title>", $title;
+
+ my $svg = $graph->burn();
+ s/^[ \t\r]+\n//gm, # remove lines with only whitespace (many useless ^M)
+ s/[ \t\r]+$//gm, # trailing whitespace
+ s/ {4}\r*/\t/g, # tabs for indenting
+ s/^(<svg width=")1600(" height=")1200("[^>]*>)/${1}100%${2}100%$3$lead/m,
+ for $svg; # cleanup xml
+
+ open my $apmfile, '>', "$APMSVG.svg";
+ print $apmfile $svg;
+}
+
+__END__
+
+=head1 NAME
+
+screp - StarCraft replay parser
+
+=head1 SYNOPSIS
+
+screp [options] < [replay data]
+
+ Options:
+ --verbose
+ --apm
+
+=head1 OPTIONS
+
+=head1 AUTHOR
+
+Mischa POSLAWSKY <perl@shiar.org>
+
+=head1 STUFF
+