X-Git-Url: http://git.shiar.nl/perl/schtarr.git/blobdiff_plain/80e893e4edd9fe3e2f3c7eae0e49d193f19f2a16..HEAD:/screp diff --git a/screp b/screp index 10a2554..29b7e0b 100755 --- a/screp +++ b/screp @@ -2,414 +2,28 @@ use strict; use warnings; use Data::Dumper; +use Data::StarCraft::Replay; + +our $VERSION = '1.01'; my $SHOWWARN = 0; +my $ACTGIF = undef; +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, + "act" => \$ACTGIF, + "dbname|D=s" => \$DBNAME, + "dbid|d=s" => \$DBGAME, ); -{ - -package Data::StarCraft::Replay; - -use Data::Dumper; - -use constant { - CMD_REPEAT => 4, -}; - -my %build = ( - 0x19 => "morph", - 0x1E => "build", - 0x1F => "warp", - 0x24 => "add-on", - 0x2E => "evolve", - 0x47 => "land", -); -my %unit = ( - 0x00 => "Marine", - 0x01 => "Ghost", - 0x02 => "Vulture", - 0x03 => "Goliath", - # undef, - 0x05 => "Siege Tank", - # undef, - 0x07 => "SCV", - 0x08 => "Wraith", - 0x09 => "Science Vessel", - # undef, - 0x0B => "Dropship", - 0x0C => "Battlecruiser", - # undef, - 0x0E => "Nuke", - # (undef) x 0x11, - 0x20 => "Firebat", - # undef, - 0x22 => "Medic", - # undef, - # undef, - 0x25 => "Zergling", - 0x26 => "Hydralisk", - 0x27 => "Ultralisk", - # undef, - 0x29 => "Drone", - 0x2A => "Overlord", - 0x2B => "Mutalisk", - 0x2C => "Guardian", - 0x2D => "Queen", - 0x2E => "Defiler", - 0x2F => "Scourge", - # undef, - # undef, - 0x32 => "Infested Terran", - # (undef) x 7, - 0x3A => "Valkyrie", - # undef, - 0x3C => "Corsair", - 0x3D => "Dark Templar", - 0x3E => "Devourer", - # undef, - 0x40 => "Probe", - 0x41 => "Zealot", - 0x42 => "Dragoon", - 0x43 => "High Templar", - # undef, - 0x45 => "Shuttle", - 0x46 => "Scout", - 0x47 => "Arbiter", - 0x48 => "Carrier", - # (undef) x 0x0A, - 0x53 => "Reaver", - 0x54 => "Observer", - # (undef) x 0x12, - 0x67 => "Lurker", - # undef, - # undef, - 0x6A => "Command Center", - 0x6B => "ComSat", - 0x6C => "Nuclear Silo", - 0x6D => "Supply Depot", - 0x6E => "Refinery", # refinery? - 0x6F => "Barracks", - 0x70 => "Academy", # Academy? - 0x71 => "Factory", - 0x72 => "Starport", - 0x73 => "Control Tower", - 0x74 => "Science Facility", - 0x75 => "Covert Ops", - 0x76 => "Physics Lab", - # undef, - 0x78 => "Machine Shop", - # undef, - 0x7A => "Engineering Bay", - 0x7B => "Armory", - 0x7C => "Missile Turret", - 0x7D => "Bunker", - # (undef) x 4, - 0x82 => "Infested CC", - 0x83 => "Hatchery", - 0x84 => "Lair", - 0x85 => "Hive", - 0x86 => "Nydus Canal", - 0x87 => "Hydralisk Den", - 0x88 => "Defiler Mound", - 0x89 => "Greater Spire", - 0x8A => "Queens Nest", - 0x8B => "Evolution Chamber", - 0x8C => "Ultralisk Cavern", - 0x8D => "Spire", - 0x8E => "Spawning Pool", - 0x8F => "Creep Colony", - 0x90 => "Spore Colony", - # undef, - 0x92 => "Sunken Colony", - # undef, - # undef, - 0x95 => "Extractor", - # (undef) x 4, - 0x9A => "Nexus", - 0x9B => "Robotics Facility", - 0x9C => "Pylon", - 0x9D => "Assimilator", - # undef, - 0x9F => "Observatory", - 0xA0 => "Gateway", - # undef, - 0xA2 => "Photon Cannon", - 0xA3 => "Citadel of Adun", - 0xA4 => "Cybernetics Core", - 0xA5 => "Templar Archives", - 0xA6 => "Forge", - 0xA7 => "Stargate", - # undef, - 0xA9 => "Fleet Beacon", - 0xAA => "Arbiter Tribunal", - 0xAB => "Robotics Support Bay", - 0xAC => "Shield Battery", - # (undef) x 0x14, - 0xC0 => "Larva", - 0xC1 => "Rine/Bat", - 0xC2 => "Dark Archon", - 0xC3 => "Archon", - 0xC4 => "Scarab", - 0xC5 => "Interceptor", - 0xC6 => "Interceptor/Scarab", -); -my @upgrade = ( - "Terran Infantry Armor", - "Terran Vehicle Plating", - "Terran Ship Plating", - "Zerg Carapace", - "Zerg Flyer Carapace", - "Protoss Ground Armor", - "Protoss Air Armor", - "Terran Infantry Weapons", - "Terran Vehicle Weapons", - "Terran Ship Weapons", - "Zerg Melee Attacks", - "Zerg Missile Attacks", - "Zerg Flyer Attacks", - "Protoss Ground Weapons", - "Protoss Air Weapons", - "Protoss Plasma Shields", - # 0x10 - "U-238 Shells (Marine Range)", - "Ion Thrusters (Vulture Speed)", - undef, - "Titan Reactor (Science Vessel Energy)", - "Ocular Implants (Ghost Sight)", - "Moebius Reactor (Ghost Energy)", - "Apollo Reactor (Wraith Energy)", - "Colossus Reactor (Battle Cruiser Energy)", - "Ventral Sacs (Overlord Transport)", - "Antennae (Overlord Sight)", - "Pneumatized Carapace (Overlord Speed)", - "Metabolic Boost (Zergling Speed)", - "Adrenal Glands (Zergling Attack)", - "Muscular Augments (Hydralisk Speed)", - "Grooved Spines (Hydralisk Range)", - "Gamete Meiosis (Queen Energy)", - # 0x20 - "Defiler Energy", - "Singularity Charge (Dragoon Range)", - "Leg Enhancement (Zealot Speed)", - "Scarab Damage", - "Reaver Capacity", - "Gravitic Drive (Shuttle Speed)", - "Sensor Array (Observer Sight)", - "Gravitic Booster (Observer Speed)", - "Khaydarin Amulet (Templar Energy)", - "Apial Sensors (Scout Sight)", - "Gravitic Thrusters (Scout Speed)", - "Carrier Capacity", - "Khaydarin Core (Arbiter Energy)", - undef, - undef, - "Argus Jewel (Corsair Energy)", - # 0x30 - undef, - "Argus Talisman (Dark Archon Energy)", - "Caduceus Reactor (Medic Energy)", - "Chitinous Plating (Ultralisk Armor)", - "Anabolic Synthesis (Ultralisk Speed)", - "Charon Boosters (Goliath Range)", -); -my @research = ( - "Stim Pack", - "Lockdown", - "EMP Shockwave", - "Spider Mines", - undef, - "Siege Tank", - undef, - "Irradiate", - "Yamato Gun", - "Cloaking Field (wraith)", - "Personal Cloaking (ghost)", - "Burrow", - undef, - "Spawn Broodling", - undef, - "Plague", - # 0x10 - "Consume", - "Ensnare", - undef, - "Psionic Storm", - "Hallucination", - "Recall", - "Stasis Field", - undef, - "Restoration", - "Disruption Web", - undef, - "Mind Control", - undef, - undef, - "Optical Flare", - "Maelstrom", - # 0x20 - "Lurker Aspect", -); -my %action = ( - 0x00 => "Move", - 0x02 => "Unallowed Move?", - 0x06 => "Force move", - 0x08 => "Attack", - 0x09 => "Gather", - 0x0E => "Attack Move", - 0x13 => "Failed Casting (?)", - 0x17 => "#23 (?)", - 0x1B => "Infest CC", - 0x22 => "Repair", - 0x27 => "Clear Rally", - 0x28 => "Set Rally", - 0x4F => "Gather", - 0x50 => "Gather", - 0x70 => "Unload", - 0x71 => "Yamato", - 0x73 => "Lockdown", - 0x77 => "Dark Swarm", - 0x78 => "Parasite", - 0x79 => "Spawn Broodling", - 0x7A => "EMP", - 0x7E => "Launch Nuke", - 0x84 => "Lay Mine", - 0x8B => "ComSat Scan", - 0x8D => "Defense Matrix", - 0x8E => "Psionic Storm", - 0x8F => "Recall", - 0x90 => "Plague", - 0x91 => "Consume", - 0x92 => "Ensnare", - 0x93 => "Stasis", - 0x94 => "Hallucination", - 0x98 => "Patrol", - 0xB1 => "Heal", - 0xB4 => "Restore", - 0xB5 => "Disruption Web", - 0xB6 => "Mind Control", - 0xB8 => "Feedback", - 0xB9 => "Optic Flare", - 0xBA => "Maelstrom", - 0xC0 => "Irradiate", -); - -my %cmdread = ( - 0x09 => ["select", 1, 2 | CMD_REPEAT], - 0x0A => ["add", 1, 2 | CMD_REPEAT], - 0x0B => ["deselect", 1, 2 | CMD_REPEAT], - 0x0C => ["build", 1, \%build, 2, 2, 2, \%unit], - 0x0D => ["vision", 2], - 0x0E => ["ally", 2, 2], - 0x13 => ["hotkey", 1, [qw"assign select"], 1], - 0x14 => ["move", 2, 2, 2, 2, 1], # 1 = queued? - 0x15 => ["action", 2, 2, 2, 2, 1, \%action, 1, [qw"normal queued"]], - 0x18 => ["cancel"], - 0x19 => ["cancel hatch"], - 0x1A => ["stop", 1], - 0x1E => ["return cargo", 1], - 0x1F => ["train", 2, \%unit], - 0x20 => ["cancel train", 2], # == 254 - 0x21 => ["cloak", 1], - 0x22 => ["decloak", 1], - 0x23 => ["hatch", 2, \%unit], - 0x25 => ["unsiege", 1], - 0x26 => ["siege", 1], - 0x27 => ["arm", 0], # scarab/interceptor - 0x28 => ["unload all", 1], - 0x29 => ["unload", 2], - 0x2A => ["merge archon", 0], - 0x2B => ["hold position", 1], - 0x2C => ["burrow", 1], - 0x2D => ["unburrow", 1], - 0x2E => ["cancel nuke", 0], - 0x2F => ["lift", 2, 2], - 0x30 => ["research", 1, \@research], - 0x31 => ["cancel research", 0], - 0x32 => ["upgrade", 1, \@upgrade], -# 0x33 => ["forge-thing??"], # right after forge select: probably unpowered, iirc cancel research - 0x35 => ["morph", 2, \%unit], - 0x36 => ["stim", 0], - 0x57 => ["part", 1, {qw"1 quit 6 drop"}], - 0x5A => ["merge dark archon", 0], -); - -sub new { - my ($class) = @_; - bless [], $class; -} - -sub _read { - my $self = shift; - my ($fh, $size, $seek) = @_; - seek *$fh, $seek, 0 if $seek; - read(*$fh, my $in, $size) eq $size or return undef; - return $in; -} - -sub open { - my $self = shift; - my ($file) = @_; - - while (not eof $file) { - local $_ = $self->_read($file, 5) - and my ($time, $size) = unpack "VC", $_ - or die "Couldn't read time block head\n"; - local $_ = $self->_read($file, $size) - and my @block = unpack "C*", $_ - or die "Couldn't read time block data\n"; - while (@block) { - my $player = shift @block; - my $cmd = shift @block; - if (not defined $cmdread{$cmd}) { - warn sprintf "command #%X not defined: %d bytes ignored\n", - $cmd, scalar @block; - push @$self, [$time, $player, "??? $cmd"] if $SHOWWARN; - last; - } - - sub readbyte { - my ($data, $byte) = @_; - my $out = shift @$data; - if (($byte & 3) == 2) { - @$data ? ($out += shift(@$data) << 8) - : warn "high byte not present\n"; - } - return $out; - } - - my @format = @{ $cmdread{$cmd} }; - my $desc = shift @format; - my @data; - for my $bit (@format) { - if (ref $bit) { - if (ref $bit eq "ARRAY") { - $data[-1] = defined $bit->[$data[-1]] ? $bit->[$data[-1]] - : "? ($data[-1])"; - } else { - $data[-1] = defined $bit->{$data[-1]} ? $bit->{$data[-1]} - : "? ($data[-1])"; - } - next; - } - $bit & 3 or next; - if ($bit & CMD_REPEAT) { - push @data, readbyte(\@block, $bit) for 1 .. shift @data; - } else { - push @data, readbyte(\@block, $bit); - } - } - $desc eq "move" and $data[2] == 0 and $desc = "rally"; - push @$self, [$time, $player, $desc, @data]; - } - } - return $self; -} +use constant { APM_FIRSTFRAME => 80 / .042 }; -} +my @race = (qw(Z T P), (undef) x 3, '-'); sub showtime { my $time = shift() * .042; @@ -426,7 +40,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 +56,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 +107,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 +118,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 +169,226 @@ for my $player (sort keys %stats) { ) if 0; } +if ($ACTGIF) { + open my $imgfile, '>', "test.gif" or die; + binmode $imgfile; + select $imgfile; + + require GD; + my $ani = GD::Image->new($head->{width}, $head->{height}); + my $bg = $ani->colorAllocate(0, 0, 0); + my @plot = ( + $ani->colorAllocate(255, 0, 0), + $ani->colorAllocate(255, 255, 0), + $ani->colorAllocate(0, 255, 0), + $ani->colorAllocate(0, 255, 255), + $ani->colorAllocate(0, 0, 255), + $ani->colorAllocate(255, 0, 255), + ); + + print $ani->gifanimbegin; +# print $ani->gifanimadd; + { + my $frame = GD::Image->new($ani->getBounds); + print $frame->gifanimadd; + my $length = 30 / .042; + my $last = 0; + for (@$map) { + my ($time, $player, $cmd, @data) = @$_; +#$time < $length * 10 or last; + while ($time > $last + $length) { + $last += $length; + print $frame->gifanimadd(0, 0, 0, 32); +# $frame = GD::Image->new($ani->getBounds); + } + if ($cmd eq "build") { + $frame->setPixel($data[1]>>5, $data[2]>>5, $plot[$player]); + } + elsif ($cmd eq "move" or $cmd eq "attack") { + $frame->setPixel($data[0]>>5, $data[1]>>5, $plot[$player]); +# if $data[2] == 0xFFFF_FFFF; + } + } +# add_frame_data($frame); + print $frame->gifanimadd; + } + print $ani->gifanimend; + select STDOUT; +} + +if ($DBGAME or $DBNAME) { + +require 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, '.' } + require 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 + --act + --dbname + --dbid + +=head1 OPTIONS + +=head1 AUTHOR + +Mischa POSLAWSKY + +=head1 STUFF +