X-Git-Url: http://git.shiar.nl/gitweb.cgi/perl/schtarr.git/blobdiff_plain/80e893e4edd9fe3e2f3c7eae0e49d193f19f2a16..d674225247c0e76e85899e5e2a5549fe0ad9c245:/screp diff --git a/screp b/screp index 10a2554..8fb41f5 100755 --- a/screp +++ b/screp @@ -3,13 +3,23 @@ use strict; use warnings; use Data::Dumper; +our $VERSION = '1.01'; + my $SHOWWARN = 0; +my $APMSVG = undef; +my $DBNAME = undef; +my $DBGAME = undef; -use Getopt::Long; +use Getopt::Long qw(:config bundling auto_version auto_help); GetOptions( "verbose|v!" => \$SHOWWARN, + "apm|a=s" => \$APMSVG, + "dbname|D=s" => \$DBNAME, + "dbid|d=s" => \$DBGAME, ); +use constant { APM_FIRSTFRAME => 80 / .042 }; + { package Data::StarCraft::Replay; @@ -310,6 +320,7 @@ my %cmdread = ( 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 @@ -411,6 +422,8 @@ sub open { } +my @race = (qw(Z T P), (undef) x 3, '-'); + sub showtime { my $time = shift() * .042; my $minutes = int($time / 60); @@ -426,7 +439,7 @@ sub unpackhash { } 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 )) @@ -442,14 +455,32 @@ $_ eq "\10"x8 . "\0"x4 or warn sprintf( ) 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}; @@ -475,8 +506,6 @@ if ($SHOWWARN) { } } -printf "duration: %s\n", showtime($map->[-1][0]); - my %cmdmacro = map {$_ => 1} ( (map {$_, "cancel $_"} qw/train build hatch research upgrade arm/, @@ -488,23 +517,26 @@ my %cmdmacro = map {$_ => 1} ( 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) { + $stats{$player}{$_} = $player[$slot[$player]]{$_} + for keys %{ $player[$slot[$player]] }; my $row = $stats{$player}; $row->{last} ||= $map->[-1][0]; -# printf("%d:%6d actions (%3d micro,%4d macro);%4d APM\n", - printf("%d:%6d actions;%4d APM\n", - $player, - $row->{actions}, +# 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) { @@ -536,3 +568,173 @@ for my $player (sort keys %stats) { ) if 0; } +use Games::StarCraft::DB; +my $Db = Games::StarCraft::DB->connect({RaiseError => 1}) + or die "No database: $DBI::errstr\n"; +sub findaccount ($) { + my ($name) = @_; + my $query = $Db->query(q{ + SELECT DISTINCT account FROM play + WHERE name = ? AND account IS NOT NULL + }, $name); + return $query->rows == 1 ? $query->list : undef; +} + +if ($DBGAME) {{ + print "\n"; + my $game = $Db->query("SELECT * FROM game WHERE id=?", $DBGAME)->hash; + if (not $game) { + printf "Database game # %d not found\n", $DBGAME; + last; + } + if ($game->{map} ne $head->{map}) { + printf "Replay map (%s) does not match database map (%s)\n", + $head->{map}, $game->{map}; + last; + } + + $Db->begin; + $Db->insert("game", { + frames => $head->{frames}, +# map => $head->{map}, +# start => time2str('%Y-%m-%d %X', $head->{time}), + # endreplay => time2str('%Y-%m-%d %X', $repstats[9]), # mtime +# durationguess => \"endreplay - start", + }); + $Db->update("play", { + name => $_->{name}, #TODO: --force + race => $_->{race}, # --force + apm => $_->{gameactions} / ($_->{last} - APM_FIRSTFRAME) * 60 / .042, + team => $_->{team}, + color => $_->{color}, + }, { + game => $DBGAME, + slot => $_->{slot}, + }) for values %stats; + $Db->commit; +}} + +if ($DBNAME) { + print "\n"; + my @repstats = stat $DBNAME or die "no rep: $!\n"; + my ($name) = $DBNAME =~ m{.*/([^.]+)}; + + my %placetxt = ( + bn => "bnet", + gr => "groningen", + md => "mdhq", + ); + my ($placeid) = $name =~ /.*([a-z]{2})/; + my $place = defined $placetxt{$placeid} ? $placetxt{$placeid} : undef; + + my $winslot; + if (@ARGV == 1 and $ARGV[0] =~ /^\d$/) { + $winslot = $ARGV[0]; + } + + $Db->begin; + $Db->insert("game", { + name => $name, + frames => $head->{frames}, + map => $head->{map}, + start => time2str('%Y-%m-%d %X', $head->{time}), + endreplay => time2str('%Y-%m-%d %X', $repstats[9]), # mtime +# durationguess => \"endreplay - start", + place => $place, + }); + my $gameid = $Db->last_insert_id((undef)x4, {sequence => "game_id_seq"}); + $Db->update("game", {durationguess => \"endreplay - start"}, {id => $gameid}); + $Db->insert("play", { + game => $gameid, + slot => $_->{slot}, + name => $_->{name}, + race => $_->{race}, + apm => $_->{gameactions} / ($_->{last} - APM_FIRSTFRAME) * 60 / .042, + team => $_->{team}, + color => $_->{color}, + account => findaccount($_->{name}), + result => defined $winslot ? $_->{slot} == $winslot ? 1 : -1 : 0, + }) for values %stats; + $Db->commit; +} + +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%s", $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/^(]*>)/${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 + --dbname + --dbid + +=head1 OPTIONS + +=head1 AUTHOR + +Mischa POSLAWSKY + +=head1 STUFF +