function loadgame() { // 4.2 [GameName] $this->game['name'] = ''; for ($i = 0; $this->data[$i] != chr(0); $i++) { $this->game['name'] .= $this->data[$i]; } $this->data = substr($this->data, $i + 2); // 0-byte ending the string + 1 unknown byte // 4.3 [Encoded String] $temp = ''; for ($i = 0; $this->data[$i] != chr(0); $i++) { if ($i % 8 == 0) { $mask = ord($this->data[$i]); } else { $temp .= chr(ord($this->data[$i]) - !($mask & 1 << $i % 8)); } } $this->data = substr($this->data, $i + 1); // 4.4 [GameSettings] $this->game['speed'] = convert_speed(ord($temp[0])); if (ord($temp[1]) & 1) { $this->game['visibility'] = convert_visibility(0); } else { if (ord($temp[1]) & 2) { $this->game['visibility'] = convert_visibility(1); } else { if (ord($temp[1]) & 4) { $this->game['visibility'] = convert_visibility(2); } else { if (ord($temp[1]) & 8) { $this->game['visibility'] = convert_visibility(3); } } } } $this->game['observers'] = convert_observers(((ord($temp[1]) & 16) == true) + 2 * ((ord($temp[1]) & 32) == true)); $this->game['teams_together'] = convert_bool(ord($temp[1]) & 64); $this->game['lock_teams'] = convert_bool(ord($temp[2])); $this->game['full_shared_unit_control'] = convert_bool(ord($temp[3]) & 1); $this->game['random_hero'] = convert_bool(ord($temp[3]) & 2); $this->game['random_races'] = convert_bool(ord($temp[3]) & 4); if (ord($temp[3]) & 64) { $this->game['observers'] = convert_observers(4); } $temp = substr($temp, 13); // 5 unknown bytes + checksum // 4.5 [Map&CreatorName] $temp = explode(chr(0), $temp); $this->game['creator'] = $temp[1]; $this->game['map'] = $temp[0]; // 4.6 [PlayerCount] $temp = unpack('Vslots', $this->data); $this->data = substr($this->data, 4); $this->game['slots'] = $temp['slots']; // 4.7 [GameType] $this->game['type'] = convert_game_type(ord($this->data[0])); $this->game['private'] = convert_bool(ord($this->data[1])); $this->data = substr($this->data, 8); // 2 bytes are unknown and 4.8 [LanguageID] is useless // 4.9 [PlayerList] while (ord($this->data[0]) == 0x16) { $this->loadplayer(); $this->data = substr($this->data, 4); } // 4.10 [GameStartRecord] $temp = unpack('Crecord_id/vrecord_length/Cslot_records', $this->data); $this->data = substr($this->data, 4); $this->game = array_merge($this->game, $temp); $slot_records = $temp['slot_records']; // 4.11 [SlotRecord] for ($i = 0; $i < $slot_records; $i++) { if ($this->header['major_v'] >= 7) { $temp = unpack('Cplayer_id/x1/Cslot_status/Ccomputer/Cteam/Ccolor/Crace/Cai_strength/Chandicap', $this->data); $this->data = substr($this->data, 9); } elseif ($this->header['major_v'] >= 3) { $temp = unpack('Cplayer_id/x1/Cslot_status/Ccomputer/Cteam/Ccolor/Crace/Cai_strength', $this->data); $this->data = substr($this->data, 8); } else { $temp = unpack('Cplayer_id/x1/Cslot_status/Ccomputer/Cteam/Ccolor/Crace', $this->data); $this->data = substr($this->data, 7); } $temp['color'] = convert_color($temp['color']); $temp['race'] = convert_race($temp['race']); $temp['ai_strength'] = convert_ai($temp['ai_strength']); if ($temp['slot_status'] == 2) { // do not add empty slots $this->players[$temp['player_id']] = array_merge($this->players[$temp['player_id']], $temp); // Tome of Retraining $this->players[$temp['player_id']]['retraining_time'] = 0; } } // 4.12 [RandomSeed] $temp = unpack('Vrandom_seed/Cselect_mode/Cstart_spots', $this->data); $this->data = substr($this->data, 6); $this->game['random_seed'] = $temp['random_seed']; $this->game['select_mode'] = convert_select_mode($temp['select_mode']); if ($temp['start_spots'] != 0xcc) { // tournament replays from battle.net website don't have this info $this->game['start_spots'] = $temp['start_spots']; } }
function parseactions($actionblock, $data_length) { $block_length = 0; $action = 0; while ($data_length) { if ($block_length) { $actionblock = substr($actionblock, $block_length); } $temp = unpack('Cplayer_id/vlength', $actionblock); $player_id = $temp['player_id']; $block_length = $temp['length'] + 3; $data_length -= $block_length; $was_deselect = false; $was_subupdate = false; $was_subgroup = false; $n = 3; while ($n < $block_length) { $prev = $action; $action = ord($actionblock[$n]); switch ($action) { // Unit/building ability (no additional parameters) // here we detect the races, heroes, units, items, buildings, // upgrades case 0x10: $this->players[$player_id]['actions'][] = $this->time; if ($this->header['major_v'] >= 13) { $n++; // ability flag is one byte longer } $itemid = strrev(substr($actionblock, $n + 2, 4)); $value = convert_itemid($itemid); if (!$value) { $this->players[$player_id]['actions_details'][convert_action('ability')]++; // handling Destroyers if (ord($actionblock[$n + 2]) == 0x33 && ord($actionblock[$n + 3]) == 0x2) { $name = substr(convert_itemid('ubsp'), 2); $this->players[$player_id]['units']['order'][$this->time] = $this->players[$player_id]['units_multiplier'] . ' ' . $name; $this->players[$player_id]['units'][$name]++; $name = substr(convert_itemid('uobs'), 2); $this->players[$player_id]['units'][$name]--; } } else { $this->players[$player_id]['actions_details'][convert_action('buildtrain')]++; if (!$this->players[$player_id]['race_detected']) { if ($race_detected = convert_race($itemid)) { $this->players[$player_id]['race_detected'] = $race_detected; } } $name = substr($value, 2); switch ($value[0]) { case 'u': // preventing duplicated units if ($this->time - $this->players[$player_id]['units_time'] > ACTION_DELAY || $itemid != $this->players[$player_id]['last_itemid'] || ($itemid == 'hpea' || $itemid == 'ewsp' || $itemid == 'opeo' || $itemid == 'uaco') && $this->time - $this->players[$player_id]['units_time'] > 0) { $this->players[$player_id]['units_time'] = $this->time; $this->players[$player_id]['units']['order'][$this->time] = $this->players[$player_id]['units_multiplier'] . ' ' . $name; $this->players[$player_id]['units'][$name] += $this->players[$player_id]['units_multiplier']; } break; case 'b': $this->players[$player_id]['buildings']['order'][$this->time] = $name; $this->players[$player_id]['buildings'][$name]++; break; case 'h': $this->players[$player_id]['heroes']['order'][$this->time] = $name; $this->players[$player_id]['heroes'][$name]['revivals']++; break; case 'a': list($hero, $ability) = explode(':', $name); $retraining_time = $this->players[$player_id]['retraining_time']; if (!$this->players[$player_id]['heroes'][$hero]['retraining_time']) { $this->players[$player_id]['heroes'][$hero]['retraining_time'] = 0; } // preventing too high levels (avoiding duplicated actions) // the second condition is mainly for games with random heroes // the third is for handling Tome of Retraining usage if (($this->time - $this->players[$player_id]['heroes'][$hero]['ability_time'] > ACTION_DELAY || !$this->players[$player_id]['heroes'][$hero]['ability_time'] || $this->time - $retraining_time < RETRAINING_TIME) && $this->players[$player_id]['heroes'][$hero]['abilities'][$retraining_time][$ability] < 3) { if ($this->time - $retraining_time > RETRAINING_TIME) { $this->players[$player_id]['heroes'][$hero]['ability_time'] = $this->time; $this->players[$player_id]['heroes'][$hero]['level']++; $this->players[$player_id]['heroes'][$hero]['abilities'][$this->players[$player_id]['heroes'][$hero]['retraining_time']][$ability]++; } else { $this->players[$player_id]['heroes'][$hero]['retraining_time'] = $retraining_time; $this->players[$player_id]['heroes'][$hero]['abilities']['order'][$retraining_time] = 'Retraining'; $this->players[$player_id]['heroes'][$hero]['abilities'][$retraining_time][$ability]++; } $this->players[$player_id]['heroes'][$hero]['abilities']['order'][$this->time] = $ability; } break; case 'i': $this->players[$player_id]['items']['order'][$this->time] = $name; $this->players[$player_id]['items'][$name]++; if ($itemid == 'tret') { $this->players[$player_id]['retraining_time'] = $this->time; } break; case 'p': // preventing duplicated upgrades if ($this->time - $this->players[$player_id]['upgrades_time'] > ACTION_DELAY || $itemid != $this->players[$player_id]['last_itemid']) { $this->players[$player_id]['upgrades_time'] = $this->time; $this->players[$player_id]['upgrades']['order'][$this->time] = $name; $this->players[$player_id]['upgrades'][$name]++; } break; default: $this->errors[$this->time] = 'Unknown ItemID at ' . convert_time($this->time) . ': ' . $value; break; } $this->players[$player_id]['last_itemid'] = $itemid; } if ($this->header['major_v'] >= 7) { $n += 14; } else { $n += 6; } break; // Unit/building ability (with target position) // Unit/building ability (with target position) case 0x11: $this->players[$player_id]['actions'][] = $this->time; if ($this->header['major_v'] >= 13) { $n++; // ability flag } if (ord($actionblock[$n + 2]) <= 0x19 && ord($actionblock[$n + 3]) == 0x0) { // basic commands $this->players[$player_id]['actions_details'][convert_action('basic')]++; } else { $this->players[$player_id]['actions_details'][convert_action('ability')]++; } $value = strrev(substr($actionblock, $n + 2, 4)); if ($value = convert_buildingid($value)) { $this->players[$player_id]['buildings']['order'][$this->time] = $value; $this->players[$player_id]['buildings'][$value]++; } if ($this->header['major_v'] >= 7) { $n += 22; } else { $n += 14; } break; // Unit/building ability (with target position and target object ID) // Unit/building ability (with target position and target object ID) case 0x12: $this->players[$player_id]['actions'][] = $this->time; if ($this->header['major_v'] >= 13) { $n++; // ability flag } if (ord($actionblock[$n + 2]) == 0x3 && ord($actionblock[$n + 3]) == 0x0) { // rightclick $this->players[$player_id]['actions_details'][convert_action('rightclick')]++; } elseif (ord($actionblock[$n + 2]) <= 0x19 && ord($actionblock[$n + 3]) == 0x0) { // basic commands $this->players[$player_id]['actions_details'][convert_action('basic')]++; } else { $this->players[$player_id]['actions_details'][convert_action('ability')]++; } if ($this->header['major_v'] >= 7) { $n += 30; } else { $n += 22; } break; // Give item to Unit / Drop item on ground // Give item to Unit / Drop item on ground case 0x13: $this->players[$player_id]['actions'][] = $this->time; if ($this->header['major_v'] >= 13) { $n++; // ability flag } $this->players[$player_id]['actions_details'][convert_action('item')]++; if ($this->header['major_v'] >= 7) { $n += 38; } else { $n += 30; } break; // Unit/building ability (with two target positions and two item IDs) // Unit/building ability (with two target positions and two item IDs) case 0x14: $this->players[$player_id]['actions'][] = $this->time; if ($this->header['major_v'] >= 13) { $n++; // ability flag } if (ord($actionblock[$n + 2]) == 0x3 && ord($actionblock[$n + 3]) == 0x0) { // rightclick $this->players[$player_id]['actions_details'][convert_action('rightclick')]++; } elseif (ord($actionblock[$n + 2]) <= 0x19 && ord($actionblock[$n + 3]) == 0x0) { // basic commands $this->players[$player_id]['actions_details'][convert_action('basic')]++; } else { $this->players[$player_id]['actions_details'][convert_action('ability')]++; } if ($this->header['major_v'] >= 7) { $n += 43; } else { $n += 35; } break; // Change Selection (Unit, Building, Area) // Change Selection (Unit, Building, Area) case 0x16: $temp = unpack('Cmode/vnum', substr($actionblock, $n + 1, 3)); if ($temp['mode'] == 0x2 || !$was_deselect) { $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('select')]++; } $was_deselect = $temp['mode'] == 0x2; $this->players[$player_id]['units_multiplier'] = $temp['num']; $n += 4 + $temp['num'] * 8; break; // Assign Group Hotkey // Assign Group Hotkey case 0x17: $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('assignhotkey')]++; $temp = unpack('Cgroup/vnum', substr($actionblock, $n + 1, 3)); $this->players[$player_id]['hotkeys'][$temp['group']]['assigned']++; $this->players[$player_id]['hotkeys'][$temp['group']]['last_totalitems'] = $temp['num']; $n += 4 + $temp['num'] * 8; break; // Select Group Hotkey // Select Group Hotkey case 0x18: $this->players[$player_id]['actions'][] = $this->time; // Won't show up in APM stats, if we comment this line $this->players[$player_id]['actions_details'][convert_action('selecthotkey')]++; $this->players[$player_id]['hotkeys'][ord($actionblock[$n + 1])]['used']++; $this->players[$player_id]['units_multiplier'] = $this->players[$player_id]['hotkeys'][ord($actionblock[$n + 1])]['last_totalitems']; $n += 3; break; // Select Subgroup // Select Subgroup case 0x19: // OR is for torunament reps which don't have build_v if ($this->header['build_v'] >= 6040 || $this->header['major_v'] > 14) { if ($was_subgroup) { // can't think of anything better (check action 0x1A) $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('subgroup')]++; // I don't have any better idea what to do when somebody binds buildings // of more than one type to a single key and uses them to train units // TODO: this is rarely executed, maybe it should go after if ($was_subgroup) {}? $this->players[$player_id]['units_multiplier'] = 1; } $n += 13; } else { if (ord($actionblock[$n + 1]) != 0 && ord($actionblock[$n + 1]) != 0xff && !$was_subupdate) { $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('subgroup')]++; } $was_subupdate = ord($actionblock[$n + 1]) == 0xff; $n += 2; } break; // some subaction holder? // version < 14b: Only in scenarios, maybe a trigger-related command // some subaction holder? // version < 14b: Only in scenarios, maybe a trigger-related command case 0x1a: // OR is for torunament reps which don't have build_v if ($this->header['build_v'] >= 6040 || $this->header['major_v'] > 14) { $n += 1; $was_subgroup = $prev == 0x19 || $prev == 0; //0 is for new blocks which start with 0x19 } else { $n += 10; } break; // Only in scenarios, maybe a trigger-related command // version < 14b: Select Ground Item // Only in scenarios, maybe a trigger-related command // version < 14b: Select Ground Item case 0x1b: // OR is for torunament reps which don't have build_v if ($this->header['build_v'] >= 6040 || $this->header['major_v'] > 14) { $n += 10; } else { $this->players[$player_id]['actions'][] = $this->time; $n += 10; } break; // Select Ground Item // version < 14b: Cancel hero revival (new in 1.13) // Select Ground Item // version < 14b: Cancel hero revival (new in 1.13) case 0x1c: // OR is for torunament reps which don't have build_v if ($this->header['build_v'] >= 6040 || $this->header['major_v'] > 14) { $this->players[$player_id]['actions'][] = $this->time; $n += 10; } else { $this->players[$player_id]['actions'][] = $this->time; $n += 9; } break; // Cancel hero revival // Remove unit from building queue // Cancel hero revival // Remove unit from building queue case 0x1d: case 0x1e: // OR is for torunament reps which don't have build_v if (($this->header['build_v'] >= 6040 || $this->header['major_v'] > 14) && $action != 0x1e) { $this->players[$player_id]['actions'][] = $this->time; $n += 9; } else { $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('removeunit')]++; $value = convert_itemid(strrev(substr($actionblock, $n + 2, 4))); $name = substr($value, 2); switch ($value[0]) { case 'u': // preventing duplicated units cancellations if ($this->time - $this->players[$player_id]['runits_time'] > ACTION_DELAY || $value != $this->players[$player_id]['runits_value']) { $this->players[$player_id]['runits_time'] = $this->time; $this->players[$player_id]['runits_value'] = $value; $this->players[$player_id]['units']['order'][$this->time] = '-1 ' . $name; $this->players[$player_id]['units'][$name]--; } break; case 'b': $this->players[$player_id]['buildings'][$name]--; break; case 'h': $this->players[$player_id]['heroes'][$name]['revivals']--; break; case 'p': // preventing duplicated upgrades cancellations if ($this->time - $this->players[$player_id]['rupgrades_time'] > ACTION_DELAY || $value != $this->players[$player_id]['rupgrades_value']) { $this->players[$player_id]['rupgrades_time'] = $this->time; $this->players[$player_id]['rupgrades_value'] = $value; $this->players[$player_id]['upgrades'][$name]--; } break; } $n += 6; } break; // Found in replays with patch version 1.04 and 1.05. // Found in replays with patch version 1.04 and 1.05. case 0x21: $n += 9; break; // Change ally options // Change ally options case 0x50: $n += 6; break; // Transfer resources // Transfer resources case 0x51: $n += 10; break; // Map trigger chat command (?) // Map trigger chat command (?) case 0x60: $n += 9; while ($actionblock[$n] != "") { $n++; } ++$n; break; // ESC pressed // ESC pressed case 0x61: $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('esc')]++; ++$n; break; // Scenario Trigger // Scenario Trigger case 0x62: if ($this->header['major_v'] >= 7) { $n += 13; } else { $n += 9; } break; // Enter select hero skill submenu for WarCraft III patch version <= 1.06 // Enter select hero skill submenu for WarCraft III patch version <= 1.06 case 0x65: $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('heromenu')]++; ++$n; break; // Enter select hero skill submenu // Enter select building submenu for WarCraft III patch version <= 1.06 // Enter select hero skill submenu // Enter select building submenu for WarCraft III patch version <= 1.06 case 0x66: $this->players[$player_id]['actions'][] = $this->time; if ($this->header['major_v'] >= 7) { $this->players[$player_id]['actions_details'][convert_action('heromenu')]++; } else { $this->players[$player_id]['actions_details'][convert_action('buildmenu')]++; } $n += 1; break; // Enter select building submenu // Minimap signal (ping) for WarCraft III patch version <= 1.06 // Enter select building submenu // Minimap signal (ping) for WarCraft III patch version <= 1.06 case 0x67: if ($this->header['major_v'] >= 7) { $this->players[$player_id]['actions'][] = $this->time; $this->players[$player_id]['actions_details'][convert_action('buildmenu')]++; $n += 1; } else { $n += 13; } break; // Minimap signal (ping) // Continue Game (BlockB) for WarCraft III patch version <= 1.06 // Minimap signal (ping) // Continue Game (BlockB) for WarCraft III patch version <= 1.06 case 0x68: if ($this->header['major_v'] >= 7) { $n += 13; } else { $n += 17; } break; // Continue Game (BlockB) // Continue Game (BlockA) for WarCraft III patch version <= 1.06 // Continue Game (BlockB) // Continue Game (BlockA) for WarCraft III patch version <= 1.06 case 0x69: // Continue Game (BlockA) // Continue Game (BlockA) case 0x6a: $this->continue_game = 1; $n += 17; break; // Pause game // Pause game case 0x1: $this->pause = true; $temp = ''; $temp['time'] = $this->time; $temp['text'] = convert_chat_mode(0xfe, $this->players[$player_id]['name']); $this->chat[] = $temp; $n += 1; break; // Resume game // Resume game case 0x2: $temp = ''; $this->pause = false; $temp['time'] = $this->time; $temp['text'] = convert_chat_mode(0xff, $this->players[$player_id]['name']); $this->chat[] = $temp; $n += 1; break; // Increase game speed in single player game (Num+) // Increase game speed in single player game (Num+) case 0x4: // Decrease game speed in single player game (Num-) // Decrease game speed in single player game (Num-) case 0x5: $n += 1; break; // Set game speed in single player game (options menu) // Set game speed in single player game (options menu) case 0x3: $n += 2; break; // Save game // Save game case 0x6: $i = 1; while ($actionblock[$n] != "") { $n++; } $n += 1; break; // Save game finished // Save game finished case 0x7: $n += 5; break; // Only in scenarios, maybe a trigger-related command // Only in scenarios, maybe a trigger-related command case 0x75: $n += 2; break; default: $temp = ''; for ($i = 3; $i < $n; $i++) { // first 3 bytes are player ID and length $temp .= sprintf('%02X', ord($actionblock[$i])) . ' '; } $temp .= '[' . sprintf('%02X', ord($actionblock[$n])) . '] '; for ($i = 1; $n + $i < strlen($actionblock); $i++) { $temp .= sprintf('%02X', ord($actionblock[$n + $i])) . ' '; } $this->errors[] = 'Unknown action at ' . convert_time($this->time) . ': 0x' . sprintf('%02X', $action) . ', prev: 0x' . sprintf('%02X', $prev) . ', dump: ' . $temp; // skip to the next CommandBlock // continue 3, not 2 because of http://php.net/manual/en/control-structures.continue.php#68193 // ('Current functionality treats switch structures as looping in regards to continue.') continue 3; } } $was_deselect = $action == 0x16; $was_subupdate = $action == 0x19; } }