TrackManiaControl/plugins/MCTeam/ServerRankingPlugin.php

561 lines
18 KiB
PHP
Raw Normal View History

<?php
namespace MCTeam;
use FML\Controls\Frame;
use FML\Controls\Quads\Quad_BgsPlayerCard;
use FML\ManiaLink;
use FML\Script\Features\Paging;
use ManiaControl\Callbacks\CallbackListener;
use ManiaControl\Callbacks\Callbacks;
use ManiaControl\Commands\CommandListener;
use ManiaControl\ManiaControl;
use ManiaControl\Manialinks\ManialinkManager;
use ManiaControl\Players\Player;
use ManiaControl\Players\PlayerManager;
use ManiaControl\Plugins\Plugin;
use ManiaControl\Statistics\StatisticCollector;
use ManiaControl\Statistics\StatisticManager;
use Maniaplanet\DedicatedServer\Structures\AbstractStructure;
/**
* ManiaControl ServerRanking Plugin
*
2014-05-03 23:49:58 +02:00
* @author ManiaControl Team <mail@maniacontrol.com>
2019-01-05 21:02:24 +01:00
* @copyright 2014-2019 ManiaControl Team
2014-05-03 23:49:58 +02:00
* @license http://www.gnu.org/licenses/ GNU General Public License, Version 3
*/
class ServerRankingPlugin implements Plugin, CallbackListener, CommandListener {
/*
* Constants
*/
const PLUGIN_ID = 6;
const PLUGIN_VERSION = 0.1;
2014-05-03 23:49:58 +02:00
const PLUGIN_NAME = 'Server Ranking Plugin';
const PLUGIN_AUTHOR = 'MCTeam';
const TABLE_RANK = 'mc_rank';
const RANKING_TYPE_RECORDS = 'Records';
const RANKING_TYPE_RATIOS = 'Ratios';
const RANKING_TYPE_POINTS = 'Points';
2014-07-04 10:34:24 +02:00
const SETTING_RANKING_TYPE = 'ServerRankings Type Records/Points/Ratios';
const SETTING_MIN_HITS_RATIO_RANKING = 'Min Hits on Ratio Rankings';
const SETTING_MIN_HITS_POINTS_RANKING = 'Min Hits on Points Rankings';
const SETTING_MIN_REQUIRED_RECORDS = 'Minimum amount of records required on Records Ranking';
const SETTING_MAX_STORED_RECORDS = 'Maximum number of records per map for calculations';
const CB_RANK_BUILT = 'ServerRankingPlugin.RankBuilt';
/*
* Private properties
*/
2014-05-03 23:49:58 +02:00
/** @var ManiaControl $maniaControl * */
private $maniaControl = null;
2014-10-24 19:54:10 +02:00
private $recordCount = 0;
/**
2014-05-03 23:49:58 +02:00
* @see \ManiaControl\Plugins\Plugin::prepare()
*/
public static function prepare(ManiaControl $maniaControl) {
}
/**
2014-05-03 23:49:58 +02:00
* @see \ManiaControl\Plugins\Plugin::getId()
*/
public static function getId() {
return self::PLUGIN_ID;
}
/**
* @see \ManiaControl\Plugins\Plugin::getName()
*/
public static function getName() {
return self::PLUGIN_NAME;
}
/**
* @see \ManiaControl\Plugins\Plugin::getVersion()
*/
public static function getVersion() {
return self::PLUGIN_VERSION;
}
/**
* @see \ManiaControl\Plugins\Plugin::getAuthor()
*/
public static function getAuthor() {
return self::PLUGIN_AUTHOR;
}
/**
* @see \ManiaControl\Plugins\Plugin::getDescription()
*/
public static function getDescription() {
return "ServerRanking Plugin, ServerRanking by an avg build from the records, per count of points, or by a multiplication from Kill/Death Ratio and Laser accuracy";
}
/**
* @see \ManiaControl\Plugins\Plugin::load()
*/
public function load(ManiaControl $maniaControl) {
$this->maniaControl = $maniaControl;
$this->initTables();
2014-08-13 11:14:29 +02:00
$this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_MIN_HITS_RATIO_RANKING, 100);
$this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_MIN_HITS_POINTS_RANKING, 15);
2014-08-13 11:14:29 +02:00
$this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_MIN_REQUIRED_RECORDS, 3);
$this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_MAX_STORED_RECORDS, 50);
2014-08-13 11:14:29 +02:00
$this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_RANKING_TYPE, $this->getRankingsTypeArray());
// Callbacks
2014-08-13 11:14:29 +02:00
$this->maniaControl->getCallbackManager()->registerCallbackListener(PlayerManager::CB_PLAYERCONNECT, $this, 'handlePlayerConnect');
$this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::ENDMAP, $this, 'handleEndMap');
// Commands
2014-08-13 11:14:29 +02:00
$this->maniaControl->getCommandManager()->registerCommandListener('rank', $this, 'command_showRank', false, 'Shows your current ServerRank.');
$this->maniaControl->getCommandManager()->registerCommandListener('nextrank', $this, 'command_nextRank', false, 'Shows the person in front of you in the ServerRanking.');
$this->maniaControl->getCommandManager()->registerCommandListener(array('topranks', 'top100'), $this, 'command_topRanks', false, 'Shows an overview of the best-ranked 100 players.');
// TODO: only update records count
$this->resetRanks();
}
2014-10-24 19:54:10 +02:00
/**
* Create necessary database tables
*/
private function initTables() {
$mysqli = $this->maniaControl->getDatabase()->getMysqli();
$query = "CREATE TABLE IF NOT EXISTS `" . self::TABLE_RANK . "` (
`PlayerIndex` int(11) NOT NULL,
`Rank` int(11) NOT NULL,
`Avg` float NOT NULL,
KEY `PlayerIndex` (`PlayerIndex`),
UNIQUE `Rank` (`Rank`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='ServerRanking';";
$mysqli->query($query);
if ($mysqli->error) {
throw new \Exception($mysqli->error);
}
}
2014-07-04 10:34:24 +02:00
/**
* Get the RankingsTypeArray
*
* @return array[]
*/
private function getRankingsTypeArray() {
2014-08-13 11:14:29 +02:00
$script = $this->maniaControl->getClient()->getScriptName();
2014-07-04 10:34:24 +02:00
2014-08-13 11:14:29 +02:00
if ($this->maniaControl->getMapManager()->getCurrentMap()->getGame() === 'tm'
2014-07-04 10:34:24 +02:00
) {
//TODO also add obstacle here as default
return array(self::RANKING_TYPE_RECORDS, self::RANKING_TYPE_POINTS, self::RANKING_TYPE_RATIOS);
} else if ($script["CurrentValue"] === 'InstaDM.Script.txt') {
return array(self::RANKING_TYPE_RATIOS, self::RANKING_TYPE_POINTS, self::RANKING_TYPE_RECORDS);
} else {
return array(self::RANKING_TYPE_POINTS, self::RANKING_TYPE_RATIOS, self::RANKING_TYPE_RECORDS);
}
}
/**
* Resets and rebuilds the Ranking
*/
private function resetRanks() {
2014-08-13 11:14:29 +02:00
$mysqli = $this->maniaControl->getDatabase()->getMysqli();
// Erase old Average Data
2014-06-14 23:57:06 +02:00
$query = "TRUNCATE TABLE `" . self::TABLE_RANK . "`;";
$mysqli->query($query);
if ($mysqli->error) {
trigger_error($mysqli->error);
}
2014-08-13 11:14:29 +02:00
$type = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_RANKING_TYPE);
2014-05-03 23:49:58 +02:00
switch ($type) {
case self::RANKING_TYPE_RATIOS:
2014-08-13 11:14:29 +02:00
$minHits = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_MIN_HITS_RATIO_RANKING);
2014-08-13 11:14:29 +02:00
$hits = $this->maniaControl->getStatisticManager()->getStatsRanking(StatisticCollector::STAT_ON_HIT, -1, $minHits);
$killDeathRatios = $this->maniaControl->getStatisticManager()->getStatsRanking(StatisticManager::SPECIAL_STAT_KD_RATIO);
$accuracies = $this->maniaControl->getStatisticManager()->getStatsRanking(StatisticManager::SPECIAL_STAT_LASER_ACC);
$ranks = array();
2014-06-17 23:55:59 +02:00
foreach ($hits as $login => $hitCount) {
if (!isset($killDeathRatios[$login]) || !isset($accuracies[$login])) {
continue;
}
2014-06-17 23:55:59 +02:00
$ranks[$login] = $killDeathRatios[$login] * $accuracies[$login] * 1000;
}
arsort($ranks);
break;
case self::RANKING_TYPE_POINTS:
2014-08-13 11:14:29 +02:00
$minHits = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_MIN_HITS_POINTS_RANKING);
$ranks = $this->maniaControl->getStatisticManager()->getStatsRanking(StatisticCollector::STAT_ON_HIT, -1, $minHits);
break;
2014-06-17 23:55:59 +02:00
case self::RANKING_TYPE_RECORDS:
// TODO: verify workable status
2014-05-27 22:32:54 +02:00
/** @var LocalRecordsPlugin $localRecordsPlugin */
2014-08-13 11:14:29 +02:00
$localRecordsPlugin = $this->maniaControl->getPluginManager()->getPlugin(__NAMESPACE__ . '\LocalRecordsPlugin');
2014-05-27 22:32:54 +02:00
if (!$localRecordsPlugin) {
return;
}
2014-08-13 11:14:29 +02:00
$requiredRecords = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_MIN_REQUIRED_RECORDS);
$maxRecords = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_MAX_STORED_RECORDS);
$query = "SELECT `playerIndex`, COUNT(*) AS `Cnt`
FROM `" . LocalRecordsPlugin::TABLE_RECORDS . "`
GROUP BY `PlayerIndex`
HAVING `Cnt` >= {$requiredRecords};";
$result = $mysqli->query($query);
$players = array();
2014-05-03 23:49:58 +02:00
while ($row = $result->fetch_object()) {
$players[$row->playerIndex] = array(0, 0); //sum, count
}
$result->free_result();
2014-08-13 11:14:29 +02:00
$maps = $this->maniaControl->getMapManager()->getMaps();
2014-05-03 23:49:58 +02:00
foreach ($maps as $map) {
$records = $localRecordsPlugin->getLocalRecords($map, $maxRecords);
2014-06-14 23:57:06 +02:00
$index = 1;
2014-05-03 23:49:58 +02:00
foreach ($records as $record) {
if (isset($players[$record->playerIndex])) {
2014-06-14 23:57:06 +02:00
$players[$record->playerIndex][0] += $index;
$players[$record->playerIndex][1]++;
}
2014-06-14 23:57:06 +02:00
$index++;
}
}
$mapCount = count($maps);
//compute each players new average score
$ranks = array();
2014-06-17 23:55:59 +02:00
foreach ($players as $playerIndex => $val) {
$sum = $val[0];
$cnt = $val[1];
// ranked maps sum + $maxRecs rank for all remaining maps
2014-06-17 23:55:59 +02:00
$ranks[$playerIndex] = ($sum + ($mapCount - $cnt) * $maxRecords) / $mapCount;
}
asort($ranks);
break;
}
if (empty($ranks)) {
return;
}
$this->recordCount = count($ranks);
//Compute each player's new average score
2014-06-14 23:57:06 +02:00
$query = "INSERT INTO `" . self::TABLE_RANK . "` VALUES ";
$index = 1;
2014-06-17 23:55:59 +02:00
foreach ($ranks as $playerIndex => $rankValue) {
$query .= '(' . $playerIndex . ',' . $index . ',' . $rankValue . '),';
2014-06-14 23:57:06 +02:00
$index++;
}
$query = substr($query, 0, strlen($query) - 1); // strip trailing ','
$mysqli->query($query);
2014-06-14 23:57:06 +02:00
if ($mysqli->error) {
trigger_error($mysqli->error);
}
}
2014-05-03 23:49:58 +02:00
/**
* @see \ManiaControl\Plugins\Plugin::unload()
*/
public function unload() {
}
/**
* Handle PlayerConnect callback
*
* @param Player $player
*/
public function handlePlayerConnect(Player $player) {
$this->showRank($player);
$this->showNextRank($player);
}
/**
* Shows the serverRank to a certain Player
*
* @param Player $player
*/
2017-05-14 09:25:52 +02:00
public function showRank(Player $player) {
$rankObj = $this->getRank($player);
2014-08-13 11:14:29 +02:00
$type = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_RANKING_TYPE);
$message = '';
if ($rankObj) {
2014-05-03 23:49:58 +02:00
switch ($type) {
case self::RANKING_TYPE_RATIOS:
2014-08-13 11:14:29 +02:00
$killDeathRatio = $this->maniaControl->getStatisticManager()->getStatisticData(StatisticManager::SPECIAL_STAT_KD_RATIO, $player->index);
$accuracy = $this->maniaControl->getStatisticManager()->getStatisticData(StatisticManager::SPECIAL_STAT_LASER_ACC, $player->index);
2014-06-17 23:55:59 +02:00
$message = '$0f3Your Server rank is $<$ff3' . $rankObj->rank . '$> / $<$fff' . $this->recordCount . '$> (K/D: $<$fff' . round($killDeathRatio, 2) . '$> Acc: $<$fff' . round($accuracy * 100) . '%$>)';
break;
case self::RANKING_TYPE_POINTS:
$message = '$0f3Your Server rank is $<$ff3' . $rankObj->rank . '$> / $<$fff' . $this->recordCount . '$> Points: $fff' . $rankObj->avg;
break;
case self::RANKING_TYPE_RECORDS:
$message = '$0f3Your Server rank is $<$ff3' . $rankObj->rank . '$> / $<$fff' . $this->recordCount . '$> Avg: $fff' . round($rankObj->avg, 2);
}
} else {
2014-05-03 23:49:58 +02:00
switch ($type) {
case self::RANKING_TYPE_RATIOS:
2014-08-13 11:14:29 +02:00
$minHits = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_MIN_HITS_RATIO_RANKING);
$message = '$0f3 You must make $<$fff' . $minHits . '$> Hits on this server before receiving a rank...';
break;
case self::RANKING_TYPE_POINTS:
2014-08-13 11:14:29 +02:00
$minPoints = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_MIN_HITS_POINTS_RANKING);
2014-05-13 16:03:26 +02:00
$message = '$0f3 You must make $<$fff' . $minPoints . '$> Hits on this server before receiving a rank...';
break;
case self::RANKING_TYPE_RECORDS:
2014-08-13 11:14:29 +02:00
$minRecords = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_MIN_REQUIRED_RECORDS);
2014-05-13 16:03:26 +02:00
$message = '$0f3 You need $<$fff' . $minRecords . '$> Records on this server before receiving a rank...';
}
}
2017-05-14 09:25:52 +02:00
$this->maniaControl->getChat()->sendChat($message, $player);
}
/**
2014-06-15 00:03:07 +02:00
* Get the Rank Object for the given Player
*
* @param Player $player
2014-06-15 00:03:07 +02:00
* @return Rank
*/
private function getRank(Player $player) {
//TODO setting global from db or local
2014-08-13 11:14:29 +02:00
$mysqli = $this->maniaControl->getDatabase()->getMysqli();
2014-06-14 23:57:06 +02:00
$query = "SELECT * FROM `" . self::TABLE_RANK . "`
WHERE `PlayerIndex` = {$player->index};";
$result = $mysqli->query($query);
if ($mysqli->error) {
trigger_error($mysqli->error);
return null;
}
if ($result->num_rows <= 0) {
$result->free_result();
return null;
}
2014-05-03 23:49:58 +02:00
$row = $result->fetch_array();
$result->free_result();
return Rank::fromArray($row);
}
2014-05-03 23:49:58 +02:00
/**
2014-06-15 00:03:07 +02:00
* Show which Player is next ranked to you
2014-05-03 23:49:58 +02:00
*
* @param Player $player
2014-05-09 17:31:29 +02:00
* @return bool
2014-05-03 23:49:58 +02:00
*/
2017-05-14 09:25:52 +02:00
public function showNextRank(Player $player) {
2014-05-03 23:49:58 +02:00
$rankObject = $this->getRank($player);
2014-05-09 17:31:29 +02:00
if (!$rankObject) {
return false;
}
2014-05-03 23:49:58 +02:00
2014-10-24 19:54:10 +02:00
$nextPlayer = null;
2014-05-09 17:31:29 +02:00
if ($rankObject->rank > 1) {
2014-10-24 19:54:10 +02:00
$nextRank = $this->getNextRank($player);
if ($nextRank) {
$nextPlayer = $this->maniaControl->getPlayerManager()->getPlayerByIndex($nextRank->playerIndex);
}
}
if ($nextPlayer) {
$message = '$0f3The next better ranked player is $fff' . $nextPlayer->getEscapedNickname() . '!';
2014-05-09 17:31:29 +02:00
} else {
2014-10-24 19:54:10 +02:00
$message = '$0f3No better ranked player.';
2014-05-03 23:49:58 +02:00
}
2017-05-14 09:25:52 +02:00
$this->maniaControl->getChat()->sendChat($message, $player);
2014-05-09 17:31:29 +02:00
return true;
2014-05-03 23:49:58 +02:00
}
/**
* Get the Next Ranked Player
*
* @param Player $player
* @return Rank
*/
private function getNextRank(Player $player) {
$rankObject = $this->getRank($player);
2014-06-15 00:05:43 +02:00
if (!$rankObject) {
return null;
}
$nextRank = $rankObject->rank - 1;
2014-08-13 11:14:29 +02:00
$mysqli = $this->maniaControl->getDatabase()->getMysqli();
2014-06-14 23:57:06 +02:00
$query = "SELECT * FROM `" . self::TABLE_RANK . "`
WHERE `Rank` = {$nextRank};";
2014-06-14 23:57:06 +02:00
$result = $mysqli->query($query);
if ($mysqli->error) {
trigger_error($mysqli->error);
return null;
}
2014-06-14 23:57:06 +02:00
if ($result->num_rows <= 0) {
$result->free();
return null;
}
$row = $result->fetch_array();
$result->free();
return Rank::fromArray($row);
}
2014-05-03 23:49:58 +02:00
/**
2014-06-15 00:05:43 +02:00
* Show Ranks on Map End
2014-05-03 23:49:58 +02:00
*/
2014-06-15 00:05:43 +02:00
public function handleEndMap() {
2014-05-03 23:49:58 +02:00
$this->resetRanks();
2014-08-13 11:14:29 +02:00
foreach ($this->maniaControl->getPlayerManager()->getPlayers() as $player) {
2014-05-03 23:49:58 +02:00
if ($player->isFakePlayer()) {
continue;
}
2017-05-12 20:15:37 +02:00
//TODO combine the following to message to one (saves half of calls)
2017-05-14 09:25:52 +02:00
$this->showRank($player);
$this->showNextRank($player);
2014-05-03 23:49:58 +02:00
}
// Trigger callback
2014-08-13 11:14:29 +02:00
$this->maniaControl->getCallbackManager()->triggerCallback(self::CB_RANK_BUILT);
2014-05-03 23:49:58 +02:00
}
/**
* Shows the current Server-Rank
*
* @param array $chatCallback
* @param Player $player
*/
public function command_showRank(array $chatCallback, Player $player) {
$this->showRank($player);
}
/**
* Show the next better ranked player
*
* @param array $chatCallback
* @param Player $player
*/
public function command_nextRank(array $chatCallback, Player $player) {
if (!$this->showNextRank($player)) {
$message = '$0f3You need to have a ServerRank first!';
2014-08-13 11:14:29 +02:00
$this->maniaControl->getChat()->sendChat($message, $player);
}
}
/**
* Handles /topranks|top100 command
*
* @param array $chatCallback
* @param Player $player
*/
public function command_topRanks(array $chatCallback, Player $player) {
$this->showTopRanksList($player);
}
/**
2014-05-09 17:31:29 +02:00
* Provide a ManiaLink window with the top ranks to the player
*
* @param Player $player
*/
private function showTopRanksList(Player $player) {
$query = "SELECT * FROM `" . self::TABLE_RANK . "`
ORDER BY `Rank` ASC LIMIT 0, 100;";
2014-08-13 11:14:29 +02:00
$mysqli = $this->maniaControl->getDatabase()->getMysqli();
$result = $mysqli->query($query);
if ($mysqli->error) {
trigger_error($mysqli->error);
2014-05-09 17:31:29 +02:00
return;
}
2014-08-13 11:14:29 +02:00
$width = $this->maniaControl->getManialinkManager()->getStyleManager()->getListWidgetsWidth();
$height = $this->maniaControl->getManialinkManager()->getStyleManager()->getListWidgetsHeight();
// create manialink
$maniaLink = new ManiaLink(ManialinkManager::MAIN_MLID);
2014-05-03 23:49:58 +02:00
$script = $maniaLink->getScript();
$paging = new Paging();
$script->addFeature($paging);
// Main frame
2014-08-13 11:14:29 +02:00
$frame = $this->maniaControl->getManialinkManager()->getStyleManager()->getDefaultListFrame($script, $paging);
2017-03-25 19:15:50 +01:00
$maniaLink->addChild($frame);
// Start offsets
$posX = -$width / 2;
$posY = $height / 2;
//Predefine description Label
2014-08-13 11:14:29 +02:00
$descriptionLabel = $this->maniaControl->getManialinkManager()->getStyleManager()->getDefaultDescriptionLabel();
2017-03-25 19:15:50 +01:00
$frame->addChild($descriptionLabel);
// Headline
$headFrame = new Frame();
2017-03-25 19:15:50 +01:00
$frame->addChild($headFrame);
$headFrame->setY($posY - 5);
$array = array('$oRank' => $posX + 5, '$oNickname' => $posX + 18, '$oAverage' => $posX + 70);
2014-08-13 11:14:29 +02:00
$this->maniaControl->getManialinkManager()->labelLine($headFrame, $array);
$index = 1;
$posY -= 10;
2014-05-15 17:45:08 +02:00
$pageFrame = null;
2014-05-03 23:49:58 +02:00
while ($rankedPlayer = $result->fetch_object()) {
if ($index % 15 === 1) {
$pageFrame = new Frame();
2017-03-25 19:15:50 +01:00
$frame->addChild($pageFrame);
$posY = $height / 2 - 10;
2017-05-10 21:21:04 +02:00
$paging->addPageControl($pageFrame);
}
$playerFrame = new Frame();
2017-03-25 19:15:50 +01:00
$pageFrame->addChild($playerFrame);
$playerFrame->setY($posY);
if ($index % 2 !== 0) {
$lineQuad = new Quad_BgsPlayerCard();
2017-03-25 19:15:50 +01:00
$playerFrame->addChild($lineQuad);
$lineQuad->setSize($width, 4);
$lineQuad->setSubStyle($lineQuad::SUBSTYLE_BgPlayerCardBig);
$lineQuad->setZ(0.001);
}
2014-08-13 11:14:29 +02:00
$playerObject = $this->maniaControl->getPlayerManager()->getPlayerByIndex($rankedPlayer->PlayerIndex);
2014-10-24 19:54:10 +02:00
$array = array($rankedPlayer->Rank => $posX + 5, $playerObject->nickname => $posX + 18, (string) round($rankedPlayer->Avg, 2) => $posX + 70);
2014-08-13 11:14:29 +02:00
$this->maniaControl->getManialinkManager()->labelLine($playerFrame, $array);
2017-05-13 23:20:31 +02:00
//TODO change labelline
$posY -= 4;
$index++;
}
// Render and display xml
2014-08-13 11:14:29 +02:00
$this->maniaControl->getManialinkManager()->displayWidget($maniaLink, $player, 'TopRanks');
}
}
/**
* Rank Structure
*/
2014-05-27 08:56:56 +02:00
// TODO: extract class to own file
class Rank extends AbstractStructure {
public $playerIndex;
public $rank;
public $avg;
}