diff --git a/MatchManagerSuite/MatchManagerPickAndBan.php b/MatchManagerSuite/MatchManagerPickAndBan.php
new file mode 100644
index 0000000..d523f39
--- /dev/null
+++ b/MatchManagerSuite/MatchManagerPickAndBan.php
@@ -0,0 +1,819 @@
+maniaControl->getChat()->sendErrorToAdmins('MatchManager Core is required to use MatchManagerPickAndBan plugin. Install it and restart Maniacontrol');
+ Logger::logError('MatchManager Core is required to use MatchManagerPickAndBan plugin. Install it and restart Maniacontrol');
+ return false;
+}
+
+/**
+ * MatchManager Pick and Ban
+ *
+ * @author Beu
+ * @license http://www.gnu.org/licenses/ GNU General Public License, Version 3
+ */
+class MatchManagerPickAndBan implements ManialinkPageAnswerListener, CallbackListener, TimerListener, Plugin {
+ /*
+ * Constants
+ */
+ const PLUGIN_ID = 223;
+ const PLUGIN_VERSION = 0.1;
+ const PLUGIN_NAME = 'MatchManager Pick & Ban';
+ const PLUGIN_AUTHOR = 'Beu';
+
+ const LOG_PREFIX = '[MatchManagerPickAndBan] ';
+
+ // Settings
+ const SETTING_ENABLE = 'Enable pick & ban';
+ const SETTING_STEPCONFIG = 'Step config';
+ const SETTING_STEPDURATION = 'Step duration';
+
+ // Other MatchManager plugin
+ const MATCHMANAGERCORE_PLUGIN = 'MatchManagerSuite\MatchManagerCore';
+ const MATCHMANAGERADMINUI_PLUGIN = 'MatchManagerSuite\MatchManagerAdminUI';
+
+ // Steps
+ const ML_STEP_OPENSETTINGS = 'MatchManagerSuite\MatchManagerPickAndBan.OpenSettings';
+ const ML_STEP_MAPCHOOSENREGEX = '/^MatchManagerPickAndBan_MapChoosen:.*/';
+
+ /*
+ * Private properties
+ */
+ private ManiaControl $maniaControl;
+ private MatchManagerCore $MatchManagerCore;
+ private MatchManagerPickAndBan_State $state;
+
+ /**
+ * @param \ManiaControl\ManiaControl $maniaControl
+ * @see \ManiaControl\Plugins\Plugin::prepare()
+ */
+ public static function prepare(ManiaControl $maniaControl) {
+ }
+
+ /**
+ * @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 'Pick & Ban plugin for MatchManager';
+ }
+
+ /**
+ * @param \ManiaControl\ManiaControl $maniaControl
+ * @return bool
+ * @see \ManiaControl\Plugins\Plugin::load()
+ */
+ public function load(ManiaControl $maniaControl) {
+ $this->maniaControl = $maniaControl;
+
+ $this->clearManialinks();
+
+ $this->MatchManagerCore = $this->maniaControl->getPluginManager()->getPlugin(self::MATCHMANAGERCORE_PLUGIN);
+
+ if ($this->MatchManagerCore == Null) {
+ throw new \Exception('MatchManager Core is needed to use ' . self::PLUGIN_NAME);
+ }
+
+ $this->state = new MatchManagerPickAndBan_State;
+
+ // Settings
+ $this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_ENABLE, true, '', 0);
+ $this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_STEPCONFIG, '', '', 0);
+ $this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_STEPDURATION, 60, '', 0);
+
+ // Callbacks
+ $this->maniaControl->getManialinkManager()->registerManialinkPageAnswerListener(self::ML_STEP_OPENSETTINGS, $this, 'handleStepOpenSettings');
+ $this->maniaControl->getManialinkManager()->registerManialinkPageAnswerRegexListener(self::ML_STEP_MAPCHOOSENREGEX, $this, 'handleStepMapChoosen');
+ $this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::AFTERINIT, $this, 'handleAfterInit');
+ $this->maniaControl->getCallbackManager()->registerCallbackListener(PluginManager::CB_PLUGIN_LOADED, $this, 'handlePluginLoaded');
+ $this->maniaControl->getCallbackManager()->registerCallbackListener($this->MatchManagerCore::CB_MATCHMANAGER_ENDMATCH, $this, 'handleEndMatch');
+ $this->maniaControl->getCallbackManager()->registerCallbackListener($this->MatchManagerCore::CB_MATCHMANAGER_STOPMATCH, $this, 'handleEndMatch');
+
+ $this->MatchManagerCore->addCanStartFunction($this, 'handleCanStartMatch');
+
+ $this->updateAdminUIMenuItems();
+
+ $this->sendStateToPlayers();
+ }
+
+ /**
+ * @see \ManiaControl\Plugins\Plugin::unload()
+ */
+ public function unload() {
+ /** @var \MatchManagerSuite\MatchManagerAdminUI|null */
+ $adminUIPlugin = $this->maniaControl->getPluginManager()->getPlugin(self::MATCHMANAGERADMINUI_PLUGIN);
+ if ($adminUIPlugin !== null) {
+ $adminUIPlugin->removeMenuItem(self::ML_STEP_OPENSETTINGS);
+ }
+
+ $this->clearManialinks();
+ }
+
+ /**
+ * Custom log function to add prefix
+ *
+ * @param mixed $message
+ */
+ private function log(mixed $message) {
+ Logger::log(self::LOG_PREFIX . $message);
+ }
+
+ /**
+ * Custom logError function to add prefix
+ *
+ * @param mixed $message
+ */
+ private function logError(mixed $message) {
+ Logger::logError(self::LOG_PREFIX . $message);
+ }
+
+ /**
+ * handle Plugin Loaded
+ */
+ public function handleAfterInit() {
+ $this->updateAdminUIMenuItems();
+ }
+
+ /**
+ * handle Plugin Loaded
+ *
+ * @param string $pluginClass
+ */
+ public function handlePluginLoaded(string $pluginClass) {
+ if ($pluginClass === self::MATCHMANAGERADMINUI_PLUGIN) {
+ $this->updateAdminUIMenuItems();
+ }
+ }
+
+ /**
+ * Add items in AdminUI plugin
+ */
+ public function updateAdminUIMenuItems() {
+ /** @var \MatchManagerSuite\MatchManagerAdminUI|null */
+ $adminUIPlugin = $this->maniaControl->getPluginManager()->getPlugin(self::MATCHMANAGERADMINUI_PLUGIN);
+ if ($adminUIPlugin === null) return;
+
+ $adminUIPlugin->removeMenuItem(self::ML_STEP_OPENSETTINGS);
+
+ $menuItem = new \MatchManagerSuite\MatchManagerAdminUI_MenuItem();
+ $menuItem->setActionId(self::ML_STEP_OPENSETTINGS)
+ ->setOrder(40)
+ ->setStyle('MeshModelerIcons')
+ ->setSubStyle('LayersFocused')
+ ->setDescription('Open Pick & Ban Settings');
+ $adminUIPlugin->addMenuItem($menuItem);
+ }
+
+ /**
+ * manage Step
+ */
+ public function manageStep() {
+ // Do random pick
+ while (
+ array_key_exists($this->state->stepIndex, $this->state->steps) &&
+ $this->state->steps[$this->state->stepIndex]->login === ""
+ ) {
+ $availableMaps = array_filter($this->state->maps, function($map) {
+ return !$map->picked && !$map->banned;
+ });
+
+ $mapUid = array_rand($availableMaps);
+
+ $this->state->maps[$mapUid]->banned = ($this->state->steps[$this->state->stepIndex]->type === MatchManagerPickAndBan_Step::STEP_BAN);
+ $this->state->maps[$mapUid]->picked = ($this->state->steps[$this->state->stepIndex]->type === MatchManagerPickAndBan_Step::STEP_PICK);
+
+ $this->state->steps[$this->state->stepIndex]->mapUid = $mapUid;
+ $this->state->stepIndex++;
+ }
+
+ $this->state->startTimestamp = time();
+ $this->state->endTimestamp = time() + $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_STEPDURATION);
+
+ $this->sendStateToPlayers();
+ }
+
+ /**
+ * handle timer
+ * @return void
+ */
+ public function handleTimer() {
+ if ($this->state->status !== MatchManagerPickAndBan_State::STATUS_INPROGRESS) {
+ $this->maniaControl->getTimerManager()->unregisterTimerListening($this, 'handleTimer');
+ return;
+ }
+
+ if ($this->state->endTimestamp < time()) {
+ if (array_key_exists($this->state->stepIndex, $this->state->steps)) {
+ $this->state->steps[$this->state->stepIndex]->login = "";
+ $this->manageStep();
+ } else {
+ $this->log("Start match");
+ $this->state->status = MatchManagerPickAndBan_State::STATUS_COMPLETED;
+ $this->MatchManagerCore->MatchStart();
+ $this->clearManialinks();
+ }
+ }
+ }
+
+ /**
+ * handle Open settings manialink step
+ *
+ * @param array $callback
+ * @param Player $player
+ */
+ public function handleStepOpenSettings(array $callback, Player $player) {
+ if ($player->authLevel <= 0) return;
+
+ $pluginMenu = $this->maniaControl->getPluginManager()->getPluginMenu();
+ if (defined("\ManiaControl\ManiaControl::ISTRACKMANIACONTROL")) {
+ $player->setCache($pluginMenu, PluginMenu::CACHE_SETTING_CLASS, "PluginMenu.Settings." . self::class);
+ } else {
+ $player->setCache($pluginMenu, PluginMenu::CACHE_SETTING_CLASS, self::class);
+ }
+ $this->maniaControl->getConfigurator()->showMenu($player, $pluginMenu);
+ }
+
+ /**
+ * handle Map Choosen
+ *
+ * @param array $callback
+ * @param Player $player
+ */
+ public function handleStepMapChoosen(array $callback, Player $player) {
+ if ($this->state->status !== MatchManagerPickAndBan_State::STATUS_INPROGRESS) return;
+ if (!array_key_exists($this->state->stepIndex, $this->state->steps)) return;
+
+ if ($this->state->steps[$this->state->stepIndex]->login !== $player->login) return;
+
+ $step = $callback[1][2];
+ $stepArray = explode(':', $step);
+
+ if (count($stepArray) !== 2) return;
+
+ $mapUid = $stepArray[1];
+
+ if (!array_key_exists($mapUid, $this->state->maps)) return;
+
+ $this->state->maps[$mapUid]->banned = ($this->state->steps[$this->state->stepIndex]->type === MatchManagerPickAndBan_Step::STEP_BAN);
+ $this->state->maps[$mapUid]->picked = ($this->state->steps[$this->state->stepIndex]->type === MatchManagerPickAndBan_Step::STEP_PICK);
+
+ $this->state->steps[$this->state->stepIndex]->mapUid = $mapUid;
+ $this->state->stepIndex++;
+
+ $this->manageStep();
+ }
+
+ /**
+ * handle MatchManagerCore Can Start Match callback
+ */
+ public function handleCanStartMatch() {
+ if (!$this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_ENABLE)) return true;
+
+ if ($this->state->status === MatchManagerPickAndBan_State::STATUS_COMPLETED) {
+ $maplist = [];
+ foreach ($this->state->steps as $step) {
+ if ($step->type !== MatchManagerPickAndBan_Step::STEP_PICK) continue;
+ if (!array_key_exists($step->mapUid, $this->state->maps)) {
+ $this->logError("Picked map doesn't exist in map list, should never happen.");
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . "Picked map doesn't exist in map list, should never happen.");
+
+ $this->state = new MatchManagerPickAndBan_State;
+ return false;
+ }
+
+
+ $maplist[] = $this->state->maps[$step->mapUid]->filePath;
+ }
+
+ $setting = $this->maniaControl->getSettingManager()->getSettingObject($this->MatchManagerCore, $this->MatchManagerCore::SETTING_MODE_MAPS);
+ $setting->value = implode(',', $maplist);
+
+ $this->log('Starting match with maps "'. $setting->value .'"');
+
+ return true;
+ }
+
+ if ($this->state->status === MatchManagerPickAndBan_State::STATUS_INPROGRESS) {
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . "Pick and Ban in progress");
+ return false;
+ }
+
+ $stepConfig = $this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_STEPCONFIG);
+ if ($stepConfig === '') {
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . "'Step config' setting must be set");
+ return false;
+ }
+
+ if ($this->maniaControl->getSettingManager()->getSettingValue($this->MatchManagerCore, $this->MatchManagerCore::SETTING_MATCH_SETTINGS_MODE) !== 'All from the plugin') {
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . "Setting mode must be in 'All from the plugin' to use Pick & Ban");
+ return false;
+ }
+
+ $mapFiles = $this->maniaControl->getSettingManager()->getSettingValue($this->MatchManagerCore, $this->MatchManagerCore::SETTING_MODE_MAPS);
+ if ($mapFiles === '') {
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . "Maps must be defined to use Pick & Ban");
+ return false;
+ }
+
+ if (count(explode(',', $mapFiles)) < count(explode(',', $stepConfig))) {
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . "You must have at least same number of maps than steps");
+ return false;
+ }
+
+ $this->log('Building Pick & Ban config');
+
+ $this->state = new MatchManagerPickAndBan_State;
+ $this->state->status = MatchManagerPickAndBan_State::STATUS_INPROGRESS;
+ $this->state->originalMapList = $mapFiles;
+
+ // build map config
+ $maps = [];
+ foreach (explode(',', $mapFiles) as $mapPath) {
+ try {
+ $mapInfo = $this->maniaControl->getClient()->getMapInfo($mapPath);
+
+ $mapObject = new MatchManagerPickAndBan_Map();
+ $mapObject->mapUid = $mapInfo->uId;
+ $mapObject->filePath = $mapPath;
+ $mapObject->name = $mapInfo->name;
+ $maps[$mapInfo->uId] = $mapObject;
+
+ } catch (Exception $e) {
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . 'Error with the Map to play "' . $mapPath . '": ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ uasort($maps, function ($a, $b) {
+ return strnatcasecmp($a->name, $b->name);
+ });
+
+ $this->state->maps = $maps;
+ $this->log(count($this->state->maps) . ' maps loaded for Pick & Ban');
+
+ // build steps
+ $steps = [];
+ foreach (explode(',', $stepConfig) as $index => $stepRaw) {
+ $stepArray = explode(':', $stepRaw);
+ if (count($stepArray) !== 2) {
+ $this->maniaControl->getChat()->sendErrorToAdmins($this->MatchManagerCore->getChatPrefix() . 'Invalid step ' . $index . 'config: ' . $stepRaw);
+ return false;
+ }
+
+ $step = new MatchManagerPickAndBan_Step;
+
+ if ($stepArray[0] === 'p') {
+ $step->type = MatchManagerPickAndBan_Step::STEP_PICK;
+ } else if ($stepArray[0] === 'b') {
+ $step->type = MatchManagerPickAndBan_Step::STEP_BAN;
+ }
+
+ if ($stepArray[1] === 'r') {
+ $step->login = "";
+ } else {
+ $step->login = $stepArray[1];
+ }
+ $steps[] = $step;
+ }
+ $this->state->steps = $steps;
+ $this->log(count($this->state->steps) . ' steps loaded for Pick & Ban');
+
+ $this->maniaControl->getChat()->sendSuccess('Starting Pick & Ban phase');
+ $this->log('Starting Pick & Ban phase');
+ $this->sendManialink();
+ $this->manageStep();
+
+ $this->maniaControl->getTimerManager()->registerTimerListening($this, 'handleTimer', 1000);
+
+ return false;
+ }
+
+ public function handleEndMatch() {
+ if ($this->state->status === MatchManagerPickAndBan_State::STATUS_COMPLETED) {
+ $setting = $this->maniaControl->getSettingManager()->getSettingObject($this->MatchManagerCore, $this->MatchManagerCore::SETTING_MODE_MAPS);
+ $setting->value = $this->state->originalMapList;
+ $this->state = new MatchManagerPickAndBan_State;
+ }
+ }
+
+ private function sendManialink(): void {
+ $manialink = '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+';
+
+ $this->maniaControl->getManialinkManager()->sendManialink($manialink);
+ }
+
+ private function sendStateToPlayers(): void {
+ $manialink = '
+
+
+';
+ $this->maniaControl->getManialinkManager()->sendManialink($manialink);
+ }
+
+ private function clearManialinks() {
+ $this->maniaControl->getManialinkManager()->hideManialink(self::getShortClassName() . ':UI');
+ $this->maniaControl->getManialinkManager()->hideManialink(self::getShortClassName() . ':State');
+ }
+
+ private function getManialinkStructures(): string {
+ return '
+#Struct K_MatchManagerPickAndBan_Step {
+ Integer type;
+ Text login;
+ Text mapUid;
+}
+
+#Struct K_MatchManagerPickAndBan_Map {
+ Text mapUid;
+ Text name;
+ Boolean picked;
+ Boolean banned;
+}
+
+#Struct K_MatchManagerPickAndBan_State {
+ Integer status;
+ Integer stepIndex;
+ Integer startTimestamp;
+ Integer endTimestamp;
+ K_MatchManagerPickAndBan_Map[Text] maps;
+ K_MatchManagerPickAndBan_Step[] steps;
+}
+';
+ }
+
+ private static function getShortClassName(): string {
+ return (new \ReflectionClass(static::class))->getShortName();
+ }
+}
+
+class MatchManagerPickAndBan_State {
+ // States
+ const STATUS_WAITING = 0;
+ const STATUS_INPROGRESS = 1;
+ const STATUS_COMPLETED = 2;
+
+ public int $status = self::STATUS_WAITING;
+ public string $originalMapList = '';
+ public int $stepIndex = 0;
+ public int $startTimestamp;
+ public int $endTimestamp;
+ public array $steps = [];
+ public array $maps = [];
+}
+
+class MatchManagerPickAndBan_Step {
+ // Steps
+ const STEP_PICK = 1;
+ const STEP_BAN = 2;
+
+ public int $type;
+ public string $login;
+ public string $mapUid;
+}
+
+class MatchManagerPickAndBan_Map {
+ public string $mapUid;
+ public string $filePath;
+ public string $name;
+ public bool $picked = false;
+ public bool $banned = false;
+}
\ No newline at end of file