diff --git a/core/Configurator/Configurator.php b/core/Configurator/Configurator.php index 7867aaf9..e5973b7e 100644 --- a/core/Configurator/Configurator.php +++ b/core/Configurator/Configurator.php @@ -18,6 +18,7 @@ use ManiaControl\Manialinks\ManialinkManager; use ManiaControl\Manialinks\ManialinkPageAnswerListener; use ManiaControl\Players\Player; use ManiaControl\Server\ServerOptionsMenu; +use ManiaControl\Server\ServerUIPropertiesMenu; use ManiaControl\Server\VoteRatiosMenu; /** @@ -51,6 +52,8 @@ class Configurator implements CallbackListener, CommandListener, ManialinkPageAn private $maniaControl = null; /** @var ServerOptionsMenu $serverOptionsMenu */ private $serverOptionsMenu = null; + /** @var ServerUIPropertiesMenu $serverUIPropertiesMenu */ + private $serverUIPropertiesMenu = null; /** @var ScriptSettings $scriptSettings */ private $scriptSettings = null; /** @var VoteRatiosMenu $voteRatiosMenu */ @@ -93,6 +96,10 @@ class Configurator implements CallbackListener, CommandListener, ManialinkPageAn $this->serverOptionsMenu = new ServerOptionsMenu($maniaControl); $this->addMenu($this->serverOptionsMenu); + // Create server UI properties menu + $this->serverUIPropertiesMenu = new ServerUIPropertiesMenu($maniaControl); + $this->addMenu($this->serverUIPropertiesMenu); + // Create script settings $this->scriptSettings = new ScriptSettings($maniaControl); $this->addMenu($this->scriptSettings); diff --git a/core/Server/ServerUIPropertiesMenu.php b/core/Server/ServerUIPropertiesMenu.php new file mode 100644 index 00000000..8cc5560f --- /dev/null +++ b/core/Server/ServerUIPropertiesMenu.php @@ -0,0 +1,488 @@ +maniaControl = $maniaControl; + $this->initTables(); + + // Callbacks + $this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::ONINIT, $this, 'onInit'); + $this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::BEGINMAP, $this, 'onBeginMap'); + $this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::SM_UIPROPERTIES, $this, 'onUIProperties'); + $this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::TM_UIPROPERTIES, $this, 'onUIProperties'); + + // Settings + $this->maniaControl->getSettingManager()->initSetting($this, self::SETTING_LOAD_DEFAULT_SERVER_UI_PROPERTIES_MAP_BEGIN, false); + + // Permissions + $this->maniaControl->getAuthenticationManager()->definePermissionLevel(self::SETTING_PERMISSION_CHANGE_SERVER_UI_PROPERTIES, AuthenticationManager::AUTH_LEVEL_ADMIN); + } + + /** + * Create all necessary database tables + * + * @return boolean + */ + private function initTables() { + $mysqli = $this->maniaControl->getDatabase()->getMysqli(); + $query = "CREATE TABLE IF NOT EXISTS `" . self::TABLE_SERVER_UI_PROPERTIES . "` ( + `index` int(11) NOT NULL AUTO_INCREMENT, + `serverIndex` int(11) NOT NULL, + `uiPropertyName` varchar(100) NOT NULL DEFAULT '', + `uiPropertyValue` varchar(500) NOT NULL DEFAULT '', + PRIMARY KEY (`index`), + UNIQUE KEY `uiProperty` (`serverIndex`, `uiPropertyName`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Server UI Properties' AUTO_INCREMENT=1;"; + + $statement = $mysqli->prepare($query); + if ($mysqli->error) { + trigger_error($mysqli->error, E_USER_ERROR); + return false; + } + $statement->execute(); + if ($statement->error) { + trigger_error($statement->error, E_USER_ERROR); + return false; + } + + $statement->close(); + return true; + } + + /** + * @see \ManiaControl\Configurator\ConfiguratorMenu::getTitle() + */ + public static function getTitle() { + return 'Server UI Properties'; + } + + /** + * Handle OnInit callback + */ + public function onInit() { + $this->gameShort = $this->maniaControl->getMapManager()->getCurrentMap()->getGame(); + $this->loadServerUIPropertiesFromDatabase(); + } + + /** + * Handle Begin Map Callback + */ + public function onBeginMap() { + if ($this->maniaControl->getSettingManager()->getSettingValue($this, self::SETTING_LOAD_DEFAULT_SERVER_UI_PROPERTIES_MAP_BEGIN)) { + $this->loadServerUIPropertiesFromDatabase(); + } + } + + /** + * Handle UI Properties Callback + * + * @param UIPropertiesBaseStructure + */ + public function onUIProperties(UIPropertiesBaseStructure $structure) { + $liveUIProperties = DataUtil::flattenArray($structure->getUiPropertiesArray(), self::CONFIGURATOR_MENU_DELIMITER); + ksort($liveUIProperties); + $this->liveUIProperties = $liveUIProperties; + } + + /** + * @see \ManiaControl\Configurator\ConfiguratorMenu::getMenu() + */ + public function getMenu($width, $height, Script $script, Player $player) { + try { + $this->loadLiveServerUIProperties(); + } catch (GameModeException $e) { + return; + } + + $paging = new Paging(); + $script->addFeature($paging); + $frame = new Frame(); + + // Config + $pagerSize = 9.; + $uiPropertyHeight = 5.; + $labelTextSize = 2; + + // Pagers + $pagerPrev = new Quad_Icons64x64_1(); + $frame->addChild($pagerPrev); + $pagerPrev->setPosition($width * 0.39, $height * -0.44, 2); + $pagerPrev->setSize($pagerSize, $pagerSize); + $pagerPrev->setSubStyle($pagerPrev::SUBSTYLE_ArrowPrev); + + $pagerNext = new Quad_Icons64x64_1(); + $frame->addChild($pagerNext); + $pagerNext->setPosition($width * 0.45, $height * -0.44, 2); + $pagerNext->setSize($pagerSize, $pagerSize); + $pagerNext->setSubStyle($pagerNext::SUBSTYLE_ArrowNext); + + $paging->addButtonControl($pagerNext); + $paging->addButtonControl($pagerPrev); + + $pageCountLabel = new Label_Text(); + $frame->addChild($pageCountLabel); + $pageCountLabel->setHorizontalAlign($pageCountLabel::RIGHT); + $pageCountLabel->setPosition($width * 0.35, $height * -0.44, 1); + $pageCountLabel->setStyle($pageCountLabel::STYLE_TextTitle1); + $pageCountLabel->setTextSize(2); + + $paging->setLabel($pageCountLabel); + + // Setting pages + $pageFrame = null; + $posY = 0.; + $index = 0; + $maxCount = (int) floor(($height - 2*$pagerSize) / $uiPropertyHeight); + + foreach ($this->liveUIProperties as $uiPropertyName => $uiPropertyValue) { + if (!isset($this->liveUIProperties[$uiPropertyName])) { + continue; + } + + if ($index % $maxCount === 0) { + $pageFrame = new Frame(); + $frame->addChild($pageFrame); + $posY = $height * 0.41; + $paging->addPageControl($pageFrame); + } + + $uiPropertyFrame = new Frame(); + $pageFrame->addChild($uiPropertyFrame); + $uiPropertyFrame->setY($posY); + + $nameLabel = new Label_Text(); + $uiPropertyFrame->addChild($nameLabel); + $nameLabel->setHorizontalAlign($nameLabel::LEFT); + $nameLabel->setX($width * -0.46); + $nameLabel->setSize($width * 0.4, $uiPropertyHeight); + $nameLabel->setStyle($nameLabel::STYLE_TextCardSmall); + $nameLabel->setTextSize($labelTextSize); + $nameLabel->setText($uiPropertyName); + + if (is_bool($uiPropertyValue)) { + // Boolean checkbox + $quad = new Quad(); + $quad->setX($width / 2 * 0.545); + $quad->setSize(4, 4); + $checkBox = new CheckBox(self::ACTION_PREFIX_SERVER_UI_PROPERTIES . $uiPropertyName, $uiPropertyValue, $quad); + $uiPropertyFrame->addChild($checkBox); + } else { + // Value entry + $entry = new Entry(); + $uiPropertyFrame->addChild($entry); + $entry->setStyle(Label_Text::STYLE_TextValueSmall); + $entry->setX($width / 2 * 0.55); + $entry->setTextSize(1); + $entry->setSize($width * 0.3, $uiPropertyHeight * 0.9); + $entry->setName(self::ACTION_PREFIX_SERVER_UI_PROPERTIES . $uiPropertyName); + $entry->setDefault($uiPropertyValue); + } + + $posY -= $uiPropertyHeight; + $index++; + } + + return $frame; + } + + /** + * @see \ManiaControl\Configurator\ConfiguratorMenu::saveConfigData() + */ + public function saveConfigData(array $configData, Player $player) { + if (!$this->maniaControl->getAuthenticationManager()->checkPermission($player, self::SETTING_PERMISSION_CHANGE_SERVER_UI_PROPERTIES)) { + $this->maniaControl->getAuthenticationManager()->sendNotAllowed($player); + return; + } + if (!$configData[3] || strpos($configData[3][0]['Name'], self::ACTION_PREFIX_SERVER_UI_PROPERTIES) !== 0) { + return; + } + + $prefixLength = strlen(self::ACTION_PREFIX_SERVER_UI_PROPERTIES); + + $newUIProperties = array(); + foreach ($configData[3] as $uiProperty) { + $uiPropertyName = substr($uiProperty['Name'], $prefixLength); + if (!isset($this->liveUIProperties[$uiPropertyName])) { + continue; + } + + if ($uiProperty['Value'] == $this->liveUIProperties[$uiPropertyName]) { + // Not changed + continue; + } + + $newUIProperties[$uiPropertyName] = $uiProperty['Value']; + settype($newUIProperties[$uiPropertyName], gettype($this->liveUIProperties[$uiPropertyName])); + } + + $success = $this->applyNewServerUIProperties($newUIProperties, $player); + if ($success) { + $this->maniaControl->getChat()->sendSuccess('Server UI Properties saved!', $player); + } else { + $this->maniaControl->getChat()->sendError('Server UI Properties Saving failed!', $player); + } + + // Reopen the Menu (delayed, so Configurator doesn't show nothing) + $this->maniaControl->getTimerManager()->registerOneTimeListening( + $this, + function () use ($player) { + $this->maniaControl->getConfigurator()->showMenu($player, $this); + }, + 100 + ); + } + + /** + * Load Settings from Database + * + * @return bool + */ + public function loadServerUIPropertiesFromDatabase() { + try { + $this->loadLiveServerUIProperties(); + } catch (GameModeException $e) { + return false; + } + + $mysqli = $this->maniaControl->getDatabase()->getMysqli(); + $serverIndex = $this->maniaControl->getServer()->index; + $query = "SELECT * FROM `" . self::TABLE_SERVER_UI_PROPERTIES . "` + WHERE serverIndex = {$serverIndex};"; + $result = $mysqli->query($query); + if ($mysqli->error) { + trigger_error($mysqli->error); + return false; + } + + $loadedUIProperties = array(); + while ($row = $result->fetch_object()) { + if (!isset($this->liveUIProperties[$row->uiPropertyName])) { + continue; + } + $loadedUIProperties[$row->uiPropertyName] = $row->uiPropertyValue; + settype($loadedUIProperties[$row->uiPropertyName], gettype($this->liveUIProperties[$row->uiPropertyName])); + } + $result->free(); + if (empty($loadedUIProperties)) { + return true; + } + + return $this->setServerUIProperties($loadedUIProperties); + } + + /** + * Triggers a callback to receive the current UI Properties of the server. + */ + private function loadLiveServerUIProperties() { + switch ($this->gameShort) { + case 'sm': + $this->maniaControl->getModeScriptEventManager()->getShootmaniaUIProperties(); + break; + case 'tm': + $this->maniaControl->getModeScriptEventManager()->getTrackmaniaUIProperties(); + break; + } + } + + /** + * Sets the given UI Properties by XML to the server. + * + * @param array $uiProperties + */ + private function setServerUIProperties(array $uiProperties) { + $xmlProperties = $this->buildXmlUIProperties($uiProperties); + switch ($this->gameShort) { + case 'sm': + $this->maniaControl->getModeScriptEventManager()->setShootmaniaUIProperties($xmlProperties); + break; + case 'tm': + $this->maniaControl->getModeScriptEventManager()->setTrackmaniaUIProperties($xmlProperties); + break; + } + } + + /** + * Builds the given UI Properties into a XML string representation. + * + * @param array $uiProperties + * @return string + */ + private function buildXmlUIProperties(array $uiProperties) { + $this->includePositions($uiProperties); + $uiProperties = DataUtil::unflattenArray($uiProperties, self::CONFIGURATOR_MENU_DELIMITER); + $uiProperties = DataUtil::implodePositions($uiProperties); + return DataUtil::buildXmlStandaloneFromArray($uiProperties, 'ui_properties'); + } + + /** + * Includes possibly missing position properties in given UI Properties. + * + * @param array &$uiProperties + */ + private function includePositions(array &$uiProperties) { + $uiPropertiesToAdd = array(); + $positions = array('x', 'y', 'z'); + foreach ($uiProperties as $key => $value) { + $keySplits = explode(self::CONFIGURATOR_MENU_DELIMITER, $key); + $numKeySplits = count($keySplits); + $keySplit = $keySplits[$numKeySplits-1]; + if (in_array($keySplit, $positions)) { + foreach ($positions as $position) { + $keySplits[$numKeySplits-1] = $position; + $keyToAdd = implode(self::CONFIGURATOR_MENU_DELIMITER, $keySplits); + if (array_key_exists($keyToAdd, $this->liveUIProperties)) { + $uiPropertiesToAdd[$keyToAdd] = $this->liveUIProperties[$keyToAdd]; + } + } + } + } + $uiProperties = $uiProperties + $uiPropertiesToAdd; + } + + /** + * Apply the Array of new Server UI Properties + * + * @param array $newUIProperties + * @param Player $player + * @return bool + */ + private function applyNewServerUIProperties(array $newUIProperties, Player $player) { + if (empty($newUIProperties)) { + return true; + } + + try { + $this->setServerUIProperties($newUIProperties); + } catch (FaultException $e) { + return false; + } + + + // Save Settings into Database + $mysqli = $this->maniaControl->getDatabase()->getMysqli(); + $query = "INSERT INTO `" . self::TABLE_SERVER_UI_PROPERTIES . "` ( + `serverIndex`, + `uiPropertyName`, + `uiPropertyValue` + ) VALUES ( + ?, ?, ? + ) ON DUPLICATE KEY UPDATE + `uiPropertyValue` = VALUES(`uiPropertyValue`);"; + $statement = $mysqli->prepare($query); + if ($mysqli->error) { + trigger_error($mysqli->error); + return false; + } + $uiPropertyDbName = null; + $uiPropertyDbValue = null; + $statement->bind_param('iss', $this->maniaControl->getServer()->index, $uiPropertyDbName, $uiPropertyDbValue); + + // Notifications + $uiPropertiesCount = count($newUIProperties); + $uiPropertyIndex = 0; + $title = $this->maniaControl->getAuthenticationManager()->getAuthLevelName($player); + $chatMessage = '$ff0' . $title . ' ' . $player->getEscapedNickname() . ' set ServerUIPropert' . ($uiPropertiesCount > 1 ? 'ies' : 'y') . ' '; + foreach ($newUIProperties as $uiPropertyName => $uiPropertyValue) { + $chatMessage .= '$<$fff' . $uiPropertyName . '$>$ff0 '; + $chatMessage .= 'to $<$fff' . $this->parseServerUIPropertyValue($uiPropertyValue) . '$>'; + + if ($uiPropertyIndex < $uiPropertiesCount-1) { + $chatMessage .= ', '; + } + + // Add To Database + $uiPropertyDbName = $uiPropertyName; + $uiPropertyDbValue = $uiPropertyValue; + $statement->execute(); + if ($statement->error) { + trigger_error($statement->error); + } + + // Trigger own callback + $this->maniaControl->getCallbackManager()->triggerCallback(self::CB_SERVERUIPROPERTY_CHANGED, $uiPropertyName, $uiPropertyValue); + + $uiPropertyIndex++; + } + $statement->close(); + + $this->maniaControl->getCallbackManager()->triggerCallback(self::CB_SERVERUIPROPERTIES_CHANGED); + + $chatMessage .= '!'; + $this->maniaControl->getChat()->sendInformation($chatMessage); + Logger::logInfo($chatMessage, true); + return true; + } + + /** + * Parse the Server UI Property to a String Representation + * + * @param mixed $value + * @return string + */ + private function parseServerUIPropertyValue($value) { + if (is_bool($value)) { + return ($value ? 'True' : 'False'); + } + return (string) $value; + } +} diff --git a/core/Utils/DataUtil.php b/core/Utils/DataUtil.php new file mode 100644 index 00000000..16803441 --- /dev/null +++ b/core/Utils/DataUtil.php @@ -0,0 +1,158 @@ +xmlStandalone = true; + + if ($root === '') { + if (count($a) != 1) { + throw new InvalidArgumentException('Array needs to have a single root!'); + } + + reset($a); + $root = key($a); + if (!is_string($root)) { + throw new InvalidArgumentException('All keys have to be strings!'); + } + $a = $a[$root]; + } + + $domRootElement = $domDocument->createElement($root); + $domDocument->appendChild($domRootElement); + self::buildXmlChildFromArray($domDocument, $domRootElement, $a); + return $domDocument->saveXML(); + } + + /** + * Build a multi-dimensional array into XML-String (recursion for children). + * + * @param \DOMDocument $domDocument + * @param \DOMElement &$domElement + * @param array $a + */ + private static function buildXmlChildFromArray(\DOMDocument $domDocument, \DOMElement &$domElement, array $a) { + foreach ($a as $key => $value) { + if (is_array($value)) { + $domSubElement = $domDocument->createElement($key); + $domElement->appendChild($domSubElement); + self::buildXmlChildFromArray($domDocument, $domSubElement, $a[$key]); + } else { + $valueString = (is_string($value) ? $value : var_export($value, true)); + $domElement->setAttribute($key, $valueString); + } + } + } + + /** + * Implodes sub-arrays with position properties. + * + * @param array $a + * @param bool $recurse + * @return array + */ + public static function implodePositions(array $a, bool $recurse = true) { + $result = array(); + foreach ($a as $key => $value) { + if (is_array($value)) { + $arrayKeys = array_keys($value); + if (in_array('x', $arrayKeys) && in_array('y', $arrayKeys)) { + $value = $value['x'].' '.$value['y'].(in_array('z', $arrayKeys) ? ' '.$value['z'] : ''); + } elseif ($recurse) { + $value = self::implodePositions($value, $recurse); + } + } + + $result[$key] = $value; + } + return $result; + } + + /** + * Transforms a multidimensional-array into a 1-dimensional with concatenated keys. + * + * @param array $a + * @param string $delimiter + * @param string $prefix (used for recursion) + * @return array + */ + public static function flattenArray(array $a, string $delimiter = '.', string $prefix = '') { + $result = array(); + foreach ($a as $key => $value) + { + if (!is_string($key)) { + throw new InvalidArgumentException('All keys have to be strings!'); + } + + $new_key = $prefix . (empty($prefix) ? '' : $delimiter) . $key; + if (is_array($value)) { + $result = array_merge($result, self::flattenArray($value, $delimiter, $new_key)); + } else { + $result[$new_key] = $value; + } + } + + return $result; + } + + /** + * Transforms a 1-dimensional array into a multi-dimensional by splitting the keys by a given delimiter. + * + * @param array $a + * @param string $delimiter + * @return array + */ + public static function unflattenArray(array $a, string $delimiter = '.') { + $result = array(); + foreach ($a as $key => $value) { + if (!is_string($key)) { + throw new InvalidArgumentException('All keys have to be strings!'); + } + + $keySplits = explode($delimiter, $key); + $numSplits = count($keySplits); + $subResult = &$result; + for ($i = 0; $i < $numSplits; $i++) { + $keySplit = $keySplits[$i]; + if ($i < $numSplits-1) { + // subarray + if (!array_key_exists($keySplit, $subResult)) { + $subResult[$keySplit] = array(); + } + if (!is_array($subResult[$keySplit])) { + throw new InvalidArgumentException(''); + } else { + $subResult = &$subResult[$keySplit]; + } + } else { + // insert value + if (array_key_exists($keySplit, $subResult)) { + throw new InvalidArgumentException('Found duplicated key!'); + } else { + $subResult[$keySplit] = $value; + } + } + } + } + return $result; + } +}