A few remarks should be made here:
PlayerLogReader
imply other events that are not fired separately: a return implies a tag and drop implies a pop. (The former is not really true for marsball returns, but the way the data is recorded you cannot distinguish between marsball returns and normal returns.)PlayerLogReader::flaglessCaptureEvent
means a marsball capture, i.e. a capture that does not affect your flag carrying status.PlayerLogReader
events, the $powers
/$newPowers
/$oldPowers
attribute may be a bitwise combination of multiple powers, so use the bitwise operators of your programming language there.SplatLogReader
, you must run MapLogReader
. Listen to the MapLogReader::heightEvent
, add one to the largest/last $newY
you get, and you have the $height
required by the SplatLogReader
constructor.SplatLogReader
are only indices. To find the actual in-game times, first run PlayerLogReader
on all players. There, listen to PlayerLogReader::dropEvent
and PlayerLogReader::popEvent
and collect all times of deaths, per team. Sort these times and remove duplicates, still per team. The splat time index can now be looked up in this array of the team.SplatLogReader::splatsEvent
receives an array of simultaneous splat locations. The order of these locations is unfortunately not always related to the order of players in the JSON. The coordinates are in pixels measured from the center of the top-left (possibly empty) tile of the map. Each tile is 40 pixels.The following example PHP script, designed to be run from the command line, reads a JSON match file, and then outputs to the console the map in Unicode art, the match timeline, and the splats of each team along with the corresponding times. It should be simple to adapt this to measure any other metric from the match data.
#!/usr/bin/php
<?php
// Copyright (c) 2015, Jeroen van der Gun
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of
// conditions and the following disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
error_reporting(E_ALL);
ini_set('display_errors', '1');
if($argc != 2)
die('Usage: php ' . $argv[0] . " <filename>\n");
require './logreader.php';
class PlayerEventHandler extends PlayerLogReader
{
protected function joinEvent($time, $newTeam)
{ global $player, $events; $events[$time][] = $player->name . ' joins team ' . $newTeam; }
protected function quitEvent($time, $oldFlag, $oldPowers, $oldTeam)
{ global $player, $events; $events[$time][] = $player->name . ' quits team ' . $oldTeam; }
protected function switchEvent($time, $oldFlag, $powers, $newTeam)
{ global $player, $events; $events[$time][] = $player->name . ' switches to team ' . $newTeam; }
protected function grabEvent($time, $newFlag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' grabs flag ' . $newFlag; }
protected function captureEvent($time, $oldFlag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' captures flag ' . $oldFlag; }
protected function flaglessCaptureEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' captures marsball'; }
protected function powerupEvent($time, $flag, $powerUp, $newPowers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' powers up ' . $powerUp; }
protected function duplicatePowerupEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' extends power'; }
protected function powerdownEvent($time, $flag, $powerDown, $newPowers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' powers down ' . $powerDown; }
protected function returnEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' returns'; }
protected function tagEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' tags'; }
protected function dropEvent($time, $oldFlag, $powers, $team)
{ global $player, $events, $pops; $events[$time][] = $player->name . ' drops flag ' . $oldFlag; $pops[$team][$time] = true; }
protected function popEvent($time, $powers, $team)
{ global $player, $events, $pops; $events[$time][] = $player->name . ' pops'; $pops[$team][$time] = true; }
protected function startPreventEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' starts preventing'; }
protected function stopPreventEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' stops preventing'; }
protected function startButtonEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' starts buttoning'; }
protected function stopButtonEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' stops buttoning'; }
protected function startBlockEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' starts blocking'; }
protected function stopBlockEvent($time, $flag, $powers, $team)
{ global $player, $events; $events[$time][] = $player->name . ' stops blocking'; }
protected function endEvent($time, $flag, $powers, $team)
{
global $player, $events;
if($team)
$events[$time][] = $player->name . ' ends in team ' . $team;
}
public function __construct()
{
global $match, $player, $events;
if($player->team)
$events[0][] = $player->name . ' starts in team ' . $player->team;
parent::__construct(base64_decode($player->events), $player->team, $match->duration);
}
}
class MapEventHandler extends MapLogReader
{
protected function heightEvent($newY)
{
global $mapHeight;
echo "\n";
$mapHeight = $newY + 1;
}
protected function tileEvent($newX, $y, $tile)
{
switch($tile)
{
case self::squareWallTile: echo '■'; break;
case self::lowerLeftDiagonalWallTile: echo '◣'; break;
case self::upperLeftDiagonalWallTile: echo '◤'; break;
case self::upperRightDiagonalWallTile: echo '◥'; break;
case self::lowerRightDiagonalWallTile: echo '◢'; break;
case self::redFlagTile: case self::blueFlagTile: case self::neutralFlagTile: echo '⚑'; break;
case self::neutralSpeedpadTile: case self::redSpeedpadTile: case self::blueSpeedpadTile: echo '⤧'; break;
case self::powerupTile: echo '◎'; break;
case self::spikeTile: echo '☼'; break;
case self::buttonTile: echo '•'; break;
case self::openGateTile: case self::closedGateTile: case self::redGateTile: case self::blueGateTile: echo '▦'; break;
case self::bombTile: echo '☢'; break;
default: echo ' ';
}
}
public function __construct()
{
global $match;
parent::__construct(base64_decode($match->map->tiles), $match->map->width);
echo "\n";
}
}
class SplatEventHandler extends SplatLogReader
{
protected function splatsEvent($splats, $time)
{
global $pops, $index;
foreach($splats as $splat)
echo timeFormat($pops[$index+1][$time]), ' (', $splat[0], ',', $splat[1], ")\n";
}
public function __construct()
{
global $team, $match, $mapHeight;
parent::__construct(base64_decode($team->splats), $match->map->width, $mapHeight);
}
}
function timeFormat($time)
{
return floor($time/3600).':'.str_pad(floor($time%3600/60),2,'0',STR_PAD_LEFT).'.'.str_pad(round($time%60/0.6),2,'0',STR_PAD_LEFT);
}
$match = json_decode(file_get_contents($argv[1]));
echo "\nMAP\n";
$mapHeight = 1;
new MapEventHandler();
$events = array();
$pops = array(1 => array(), array());
foreach($match->players as $player)
new PlayerEventHandler();
echo "\nTIMELINE\n";
ksort($events);
foreach($events as $time => $timeEvents)
foreach($timeEvents as $message)
echo timeFormat($time), ' ', $message, "\n";
foreach($match->teams as $index => $team)
{
echo "\nTEAM ", $index+1, " SPLATS\n";
ksort($pops[$index+1]);
$pops[$index+1] = array_keys($pops[$index+1]);
$splats = array();
new SplatEventHandler();
}
echo "\n";
?>
Rather than downloading data from individual matches, you can also download the entire database in bulk. The bulk data is split into two JSON files, maps and matches:
Here, matches and maps are coupled to a key indicating their id. Matches have a mapId
property indicating the map id. Which files you need depends on the type of analysis you want to do. Note that the splat data is located in the Matches file, although splat data cannot be read without the Maps file.
In addition to the previous PHP code, we also have a C++ implementation of all decoders. This is especially handy for processing the large bulk data files efficiently. Download this zip file, which contains the above blob readers in tagpro.h
, base64-functionality in base64.h
and a JSON parser in json.h
. The file test.cpp
is an example program processing the bulk data that you can use as a starting point for your own.
Note that your compiler must support C++11 or later. For some compilers, e.g. GCC, you may need to set a command-line flag to enable C++11 support.
Prior to TagPro Analytics, there used to be the tagpro.me website run by bluesoul. We have merged his data into our database. Although these old matches cannot be downloaded individually, they are available in the following bulk JSON file:
This can be used with the same bulk maps file as above. Be aware that there are important differences in the data format; consequently, your analyses cannot be as advanced as with TagPro Analytics. The differences are outlined in the last column of the table at the top of this page. All match ids of tagpro.me matches start with the letter b
. Special match #b1277368 did not originate from the tagpro.me dataset but follows the same format.
If you want, you can download the original CSV file published by bluesoul as a torrent. However, it is probably more useful to use our transformation of this dataset, as the original publication contains many incomplete, duplicate and partly corrupted records, which we repaired or removed as appropriate. It may be helpful to know that the match id on TagPro Analytics is equal to the id of the first player of a match in the original dataset.
If you have the PNG and JSON files of a map in standard TagPro format, select them together in the form below in order to render the map and output a converted map description in TagPro Analytics format.
XML Sitemaps are provided for the match page URLs of this website, allowing you to quickly see which match ids are in use. Each Sitemap contains a specific range of match ids of at most 50,000 matches each. The URLs of the Sitemaps can be found in the Sitemap Index, which is extended every time a new Sitemap has been filled. The first 15 Sitemaps are for the old tagpro.me matches.
We understand that this requires quite some effort, but you should be able to produce awesome data analyses using this dataset. Please credit us as your data source when you publish any analysis, so that other people can find out too about this service.
Finally, whatever you do or want to do with this dataset, please check out and subscribe to /r/tagprostatistics. Please post there if you have anything to share, so that everybody is aware of what others are doing.