iControl = $iControl; // Load config $this->config = Tools::loadConfig('united.plugin.xml'); $this->loadSettings(); // Check for enabled setting if (!$this->settings->enabled) return; // Load clients $this->loadClients(); // Register for callbacks $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_ONINIT, $this, 'handleOnInitCallback'); $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_5_SECOND, $this, 'handle5Seconds'); $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERCONNECT, $this, 'handlePlayerConnect'); $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERMANIALINKPAGEANSWER, $this, 'handleManialinkPageAnswer'); // Register for commands $this->iControl->commands->registerCommandHandler('nextserver', $this, 'handleNextServerCommand'); if ($this->settings->widgets_enabled) { // Build addfavorite manialink $this->buildFavoriteManialink(); } error_log('United Pugin v' . self::VERSION . ' ready!'); } /** * Handle iControl OnInit callback * * @param array $callback */ public function handleOnInitCallback($callback) { if ($this->settings->widgets_enabled) { // Send widgets to all players if (Tools::toBool($this->config->widgets->addfavorite->enabled)) { // Send favorite widget if (!$this->iControl->client->query('SendDisplayManialinkPage', $this->manialinks[self::ML_ADDFAVORITE]->asXml(), 0, false)) { trigger_error("Couldn't send favorite widget! " . $this->iControl->getClientErrorText()); } } } } /** * Load settings from config */ private function loadSettings() { $this->settings = new \stdClass(); // Enabled $this->settings->enabled = Tools::toBool($this->config->enabled); // Timeout $timeout = $this->iControl->server->config->xpath('timeout'); if ($timeout) { $this->settings->timeout = (int) $timeout[0]; } else { $this->settings->timeout = 30; } // Game mode $mode = $this->config->xpath('mode'); if ($mode) { $mode = (int) $mode[0]; if ($mode < 1 || $mode > 6) { $this->settings->gamemode = 2; } else { $this->settings->gamemode = $mode; } } // Server status $hide_game_server = $this->config->xpath('hide_game_server'); if ($hide_game_server) { $this->settings->hide_game_server = Tools::toBool($hide_game_server[0]); } else { $this->settings->hide_game_server = true; } // Passwords $lobbyPassword = $this->config->xpath('lobbies/password'); if ($lobbyPassword) { $this->settings->lobbyPassword = (string) $lobbyPassword[0]; } else { $this->settings->lobbyPassword = ''; } $gamePassword = $this->config->xpath('gameserver/password'); if ($gamePassword) { $this->settings->gamePassword = (string) $gamePassword[0]; } else { $this->settings->gamePassword = ''; } // Widgets $this->settings->widgets_enabled = Tools::toBool($this->config->widgets->enabled); } /** * Loop events on clients */ public function loop() { if (!$this->settings->enabled) return; // Check callbacks all clients $clients = array_merge($this->gameServer, $this->lobbies); $currentServer = $this->gameServer[$this->currentClientIndex]; foreach ($clients as $index => $client) { $client->resetError(); $client->readCB(); $callbacks = $client->getCBResponses(); if (!is_array($callbacks) || $client->isError()) { trigger_error("Error reading server callbacks! " . $this->iControl->getClientErrorText($client)); } else { if ($client == $currentServer) { // Currently active game server foreach ($callbacks as $index => $callback) { $callbackName = $callback[0]; switch ($callbackName) { case Callbacks::CB_MP_ENDMAP: { $this->switchToNextServer(false); break; } } } if ($this->lastStatusCheck + 2 > time()) continue; $this->lastStatusCheck = time(); if (!$client->query('CheckEndMatchCondition')) { trigger_error("Couldn't get game server status. " . $this->iControl->getClientErrorText($client)); } else { $response = $client->getResponse(); switch ($response) { case 'Finished': { if ($this->finishedBegin < 0) { $this->finishedBegin = time(); } else if ($this->finishedBegin + 13 <= time()) { $this->switchToNextServer(true); } break; } default: { $this->finishedBegin = -1; break; } } } } else { // Lobby or inactive game server -> Redirect players foreach ($callbacks as $callback) { switch ($callback[0]) { case Callbacks::CB_MP_PLAYERCONNECT: { $this->playerJoinedLobby($client, $callback); break; } } } } } } // Check for switch server request if ($this->switchServerRequested > 0 && $this->switchServerRequested <= time()) { $this->switchServerRequested = -1; // Switch server $this->switchToNextServer(true); } } /** * Handle 5 seconds callback */ public function handle5Seconds($callback = null) { // Update lobby infos $players = $this->iControl->server->getPlayers(); if (is_array($players)) { $playerCount = count($players); $playerLevel = 0.; if ($playerCount > 0) { foreach ($players as $player) { $playerLevel += $player['LadderRanking']; } $playerLevel /= $playerCount; } foreach ($this->lobbies as $lobby) { if (!$lobby->query('SetLobbyInfo', true, $playerCount, 255, $playerLevel)) { trigger_error("Couldn't update lobby info. " . $this->iControl->getClientErrorText($lobby)); } } } // Check for not-redirected players $clients = array_merge($this->gameServer, $this->lobbies); $joinLink = $this->getJoinLink(); foreach ($clients as $client) { if ($client == $this->gameServer[$this->currentClientIndex]) continue; $players = $this->iControl->server->getPlayers($client); if (!is_array($players)) continue; foreach ($players as $player) { $login = $player['Login']; if (!$client->query('SendOpenLinkToLogin', $login, $joinLink, 1)) { trigger_error( "Couldn't redirect player '" . $login . "' to active game server. " . $this->iControl->getClientErrorText($client)); } } } } /** * Handle player manialink page answer callback */ public function handleManialinkPageAnswer($callback) { $login = $callback[1][1]; $action = $callback[1][2]; switch ($action) { case self::ML_ADDFAVORITE: { // Open manialink to add server logins to favorite $serverLogins = array(); $add_all = Tools::toBool($this->config->widgets->addfavorite->add_all); if ($add_all) { // Add all server foreach ($this->gameServer as $serverClient) { array_push($serverLogins, $this->iControl->server->getLogin($serverClient)); } foreach ($this->lobbies as $serverClient) { array_push($serverLogins, $this->iControl->server->getLogin($serverClient)); } } else { // Add only current server array_push($serverLogins, $this->iControl->server->getLogin()); } // Build manialink url $manialink = 'icontrol?favorite'; foreach ($serverLogins as $serverLogin) { $manialink .= '&' . $serverLogin; } // Send url to player if (!$this->iControl->client->query('SendOpenLinkToLogin', $login, $manialink, 1)) { trigger_error( "Couldn't open manialink to add server to favorite for '" . $login . "'! " . $this->iControl->getClientErrorText()); } break; } } } /** * Switch to the next server * * @param bool $simulateMapEnd * Simulate end of the map by sending callbacks */ private function switchToNextServer($simulateMapEnd) { $this->finishedBegin = -1; $oldClient = $this->gameServer[$this->currentClientIndex]; $random_order = Tools::toBool($this->config->random_order); if ($random_order) { // Random next server $this->currentClientIndex = rand(0, count($this->gameServer) - 1); } else { // Next server in list $this->currentClientIndex++; } if ($this->currentClientIndex >= count($this->gameServer)) $this->currentClientIndex = 0; $newClient = $this->gameServer[$this->currentClientIndex]; if ($newClient == $oldClient) return; // Restart map on next game server if (!$newClient->query('RestartMap')) { trigger_error("Couldn't restart map on next game server. " . $this->iControl->getClientErrorText($newClient)); } if ($simulateMapEnd) { // Simulate EndMap on old client $this->iControl->callbacks->triggerCallback(Callbacks::CB_IC_ENDMAP, array(Callbacks::CB_IC_ENDMAP)); } // Transfer players to next server $joinLink = $this->getJoinLink($newClient); if (!$oldClient->query('GetPlayerList', 255, 0)) { trigger_error("Couldn't get player list. " . $this->iControl->getClientErrorText($oldClient)); } else { $playerList = $oldClient->getResponse(); foreach ($playerList as $player) { $login = $player['Login']; if (!$oldClient->query('SendOpenLinkToLogin', $login, $joinLink, 1)) { trigger_error("Couldn't redirect player to next game server. " . $this->iControl->getClientErrorText($oldClient)); } } $this->iControl->client = $newClient; } // Trigger client updated callback $this->iControl->callbacks->triggerCallback(Callbacks::CB_IC_CLIENTUPDATED, "Plugin_United.SwitchedServer"); if ($simulateMapEnd) { // Simulate BeginMap on new client $map = $this->iControl->server->getMap(); if ($map) { $this->iControl->callbacks->triggerCallback(Callbacks::CB_IC_BEGINMAP, array(Callbacks::CB_IC_BEGINMAP, array($map))); } } } /** * Handle nextserver command * * @param mixed $command */ public function handleNextServerCommand($command) { if (!$command) return; $login = $command[1][1]; if (!$this->iControl->authentication->checkRight($login, 'operator')) { // Not allowed $this->iControl->authentication->sendNotAllowed($login); return; } // Request skip to next server $this->switchServerRequested = time() + 3; // Send chat message $this->iControl->chat->sendInformation("Switching to next server in 3 seconds..."); } /** * Handle PlayerConnect callback */ public function playerJoinedLobby($client, $callback) { if (!$client) return; $data = $callback[1]; $login = $data[0]; // Redirect player to current game server $gameserver = $this->gameServer[$this->currentClientIndex]; $joinLink = $this->getJoinLink($gameserver, !$data[1]); if (!$client->query('SendOpenLinkToLogin', $login, $joinLink, 1)) { trigger_error( "United Plugin: Couldn't redirect player to current game server. " . $this->iControl->getClientErrorText($client)); } } /** * Connect to the game server defined in the config */ private function loadClients() { $gameserver = $this->config->xpath('gameserver/server'); $lobbies = $this->config->xpath('lobbies/server'); $clientsConfig = array_merge($gameserver, $lobbies); foreach ($clientsConfig as $index => $serv) { $isGameServer = (in_array($serv, $gameserver)); $host = $serv->xpath('host'); $port = $serv->xpath('port'); if (!$host || !$port) { trigger_error("Invalid configuration!", E_USER_ERROR); } $host = (string) $host[0]; $port = (string) $port[0]; error_log("Connecting to united " . ($isGameServer ? 'game' : 'lobby') . " server at " . $host . ":" . $port . "..."); $client = new \IXR_ClientMulticall_Gbx(); // Connect if (!$client->InitWithIp($host, $port, $this->settings->timeout)) { trigger_error( "Couldn't connect to united " . ($isGameServer ? 'game' : lobby) . " server! " . $client->getErrorMessage() . "(" . $client->getErrorCode() . ")", E_USER_ERROR); } $login = $serv->xpath('login'); $pass = $serv->xpath('pass'); if (!$login || !$pass) { trigger_error("Invalid configuration!", E_USER_ERROR); } $login = (string) $login[0]; $pass = (string) $pass[0]; // Authenticate if (!$client->query('Authenticate', $login, $pass)) { trigger_error( "Couldn't authenticate on united " . ($isGameServer ? 'game' : 'lobby') . " server with user '" . $login . "'! " . $client->getErrorMessage() . "(" . $client->getErrorCode() . ")", E_USER_ERROR); } // Enable callback system if (!$client->query('EnableCallbacks', true)) { trigger_error("Couldn't enable callbacks! " . $client->getErrorMessage() . "(" . $client->getErrorCode() . ")", E_USER_ERROR); } // Wait for server to be ready if (!$this->iControl->server->waitForStatus($client, 4)) { trigger_error("Server couldn't get ready!", E_USER_ERROR); } // Set api version if (!$client->query('SetApiVersion', iControl::API_VERSION)) { trigger_error( "Couldn't set API version '" . iControl::API_VERSION . "'! This might cause problems. " . $this->iControl->getClientErrorText($client)); } // Set server settings $password = ($isGameServer ? $this->settings->gamePassword : $this->settings->lobbyPassword); $hideServer = ($isGameServer && $this->settings->hide_game_server ? 1 : 0); // Passwords if (!$client->query('SetServerPassword', $password)) { trigger_error("Couldn't set server join password. " . $this->iControl->getClientErrorText($client)); } if (!$client->query('SetServerPasswordForSpectator', $password)) { trigger_error("Couldn't set server spec password. " . $this->iControl->getClientErrorText($client)); } // Show/Hide server if (!$client->query('SetHideServer', $hideServer)) { trigger_error( "Couldn't set server '" . ($hideServer == 0 ? 'shown' : 'hidden') . "'. " . $this->iControl->getClientErrorText($client)); } // Enable service announces if (!$client->query("DisableServiceAnnounces", false)) { trigger_error("Couldn't enable service announces. " . $this->iControl->getClientErrorText($client)); } // Set game mode if (!$client->query('SetGameMode', $this->settings->gamemode)) { trigger_error( "Couldn't set game mode (" . $this->settings->gamemode . "). " . $this->iControl->getClientErrorText($client)); } else if (!$client->query('RestartMap')) { trigger_error("Couldn't restart map to change game mode. " . $this->iControl->getClientErrorText($client)); } // Save client $client->index = $index; if ($isGameServer) { array_push($this->gameServer, $client); if (count($this->gameServer) === 1) { $this->iControl->client = $client; } } else { array_push($this->lobbies, $client); } } error_log("United Plugin: Connected to all game and lobby server!"); } /** * Handle PlayerConnect callback * * @param array $callback */ public function handlePlayerConnect($callback) { if ($this->settings->widgets_enabled) { // Send manialinks to the client $login = $callback[1][0]; if (Tools::toBool($this->config->widgets->addfavorite->enabled)) { // Send favorite widget if (!$this->iControl->client->query('SendDisplayManialinkPageToLogin', $login, $this->manialinks[self::ML_ADDFAVORITE]->asXml(), 0, false)) { trigger_error("Couldn't send favorite widget to player '" . $login . "'! " . $this->iControl->getClientErrorText()); } } } } /** * Build join link for the given client */ private function getJoinLink(&$client = null, $play = true) { if (!$client) { $client = $this->gameServer[$this->currentClientIndex]; } if (!$client->query('GetSystemInfo')) { trigger_error("Couldn't fetch server system info. " . $this->iControl->getClientErrorText($client)); return null; } else { $systemInfo = $client->getResponse(); $password = ''; if (!$client->query('GetServerPassword')) { trigger_error("Couldn't get server password. " . $this->iControl->getClientErrorText($client)); } else { $password = $client->getResponse(); } return '#q' . ($play ? 'join' : 'spectate') . '=' . $systemInfo['ServerLogin'] . (strlen($password) > 0 ? ':' . $password : '') . '@' . $systemInfo['TitleId']; } } /** * Build manialink for addfavorite button */ private function buildFavoriteManialink() { // Load configs $config = $this->config->widgets->addfavorite; if (!Tools::toBool($config->enabled)) return; $pos_x = (float) $config->pos_x; $pos_y = (float) $config->pos_y; $height = (float) $config->height; $width = (float) $config->width; $add_all = Tools::toBool($config->add_all); // Build manialink $xml = Tools::newManialinkXml(self::ML_ADDFAVORITE); $frameXml = $xml->addChild('frame'); $frameXml->addAttribute('posn', $pos_x . ' ' . $pos_y); // Background $quadXml = $frameXml->addChild('quad'); Tools::addAlignment($quadXml); $quadXml->addAttribute('posn', '0 0 0'); $quadXml->addAttribute('sizen', $width . ' ' . $height); $quadXml->addAttribute('style', 'Bgs1InRace'); $quadXml->addAttribute('substyle', 'BgTitleShadow'); $quadXml->addAttribute('action', self::ML_ADDFAVORITE); // Heart $quadXml = $frameXml->addChild('quad'); Tools::addAlignment($quadXml); $quadXml->addAttribute('id', 'Quad_AddFavorite'); $quadXml->addAttribute('posn', '0 0 1'); $quadXml->addAttribute('sizen', ($width - 1.) . ' ' . ($height - 0.8)); $quadXml->addAttribute('style', 'Icons64x64_1'); $quadXml->addAttribute('substyle', 'StateFavourite'); $quadXml->addAttribute('scriptevents', '1'); // Tooltip $tooltipFrameXml = $frameXml->addChild('frame'); $tooltipFrameXml->addAttribute('id', 'Frame_FavoriteTooltip'); $tooltipFrameXml->addAttribute('posn', '0 ' . ($pos_y >= 0 ? '-' : '') . '13'); $tooltipFrameXml->addAttribute('hidden', '1'); $quadXml = $tooltipFrameXml->addChild('quad'); Tools::addAlignment($quadXml); $quadXml->addAttribute('posn', '0 0 2'); $quadXml->addAttribute('sizen', '28 16'); $quadXml->addAttribute('style', 'Bgs1InRace'); $quadXml->addAttribute('substyle', 'BgTitleShadow'); $labelXml = $tooltipFrameXml->addChild('label'); Tools::addAlignment($labelXml); Tools::addTranslate($labelXml); $labelXml->addAttribute('posn', '0 0 3'); $labelXml->addAttribute('sizen', '26 0'); $labelXml->addAttribute('style', 'TextTitle1'); $labelXml->addAttribute('textsize', '2'); $labelXml->addAttribute('autonewline', '1'); $countText = ''; if ($add_all) { $count = count($this->gameServer) + count($this->lobbies); $countText = 'all ' . $count . ' '; } $labelXml->addAttribute('text', 'Add ' . $countText . 'server to Favorite!'); // Script for tooltip $script = ' declare Frame_FavoriteTooltip <=> (Page.GetFirstChild("Frame_FavoriteTooltip") as CMlFrame); while (True) { yield; foreach (Event in PendingEvents) { switch (Event.Type) { case CMlEvent::Type::MouseOver: { switch (Event.ControlId) { case "Quad_AddFavorite": { Frame_FavoriteTooltip.Visible = True; } } } case CMlEvent::Type::MouseOut: { switch (Event.ControlId) { case "Quad_AddFavorite": { Frame_FavoriteTooltip.Visible = False; } } } } } }'; $xml->addChild('script', $script); $this->manialinks[self::ML_ADDFAVORITE] = $xml; } } ?>