+package Data::StarCraft::Replay;
+
+use strict;
+use warnings;
+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],
+# 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
+ 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;
+}
+
+1;
+