server = $server; $this->commandMap = $commandMap; $this->pluginDataDirectory = $pluginDataDirectory; if($this->pluginDataDirectory !== null){ if(!file_exists($this->pluginDataDirectory)){ @mkdir($this->pluginDataDirectory, 0777, true); }elseif(!is_dir($this->pluginDataDirectory)){ throw new \RuntimeException("Plugin data path $this->pluginDataDirectory exists and is not a directory"); } } } /** * @param string $name * * @return null|Plugin */ public function getPlugin(string $name){ if(isset($this->plugins[$name])){ return $this->plugins[$name]; } return null; } /** * @param PluginLoader $loader */ public function registerInterface(PluginLoader $loader) : void{ $this->fileAssociations[get_class($loader)] = $loader; } /** * @return Plugin[] */ public function getPlugins() : array{ return $this->plugins; } private function getDataDirectory(string $pluginPath, string $pluginName) : string{ if($this->pluginDataDirectory !== null){ return $this->pluginDataDirectory . $pluginName; } return dirname($pluginPath) . DIRECTORY_SEPARATOR . $pluginName; } /** * @param string $path * @param PluginLoader[] $loaders * * @return Plugin|null */ public function loadPlugin(string $path, array $loaders = null) : ?Plugin{ foreach($loaders ?? $this->fileAssociations as $loader){ if($loader->canLoadPlugin($path)){ $description = $loader->getPluginDescription($path); if($description instanceof PluginDescription){ $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.plugin.load", [$description->getFullName()])); try{ $description->checkRequiredExtensions(); }catch(PluginException $ex){ $this->server->getLogger()->error($ex->getMessage()); return null; } $dataFolder = $this->getDataDirectory($path, $description->getName()); if(file_exists($dataFolder) and !is_dir($dataFolder)){ $this->server->getLogger()->error("Projected dataFolder '" . $dataFolder . "' for " . $description->getName() . " exists and is not a directory"); return null; } if(!file_exists($dataFolder)){ mkdir($dataFolder, 0777, true); } $prefixed = $loader->getAccessProtocol() . $path; $loader->loadPlugin($prefixed); $mainClass = $description->getMain(); if(!class_exists($mainClass, true)){ $this->server->getLogger()->error("Main class for plugin " . $description->getName() . " not found"); return null; } if(!is_a($mainClass, Plugin::class, true)){ $this->server->getLogger()->error("Main class for plugin " . $description->getName() . " is not an instance of " . Plugin::class); return null; } try{ /** * @var Plugin $plugin * @see Plugin::__construct() */ $plugin = new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed); $this->plugins[$plugin->getDescription()->getName()] = $plugin; $pluginCommands = $this->parseYamlCommands($plugin); if(count($pluginCommands) > 0){ $this->commandMap->registerAll($plugin->getDescription()->getName(), $pluginCommands); } return $plugin; }catch(\Throwable $e){ $this->server->getLogger()->logException($e); return null; } } } } return null; } /** * @param string $directory * @param array $newLoaders * * @return Plugin[] */ public function loadPlugins(string $directory, array $newLoaders = null){ if(is_dir($directory)){ $plugins = []; $loadedPlugins = []; $dependencies = []; $softDependencies = []; if(is_array($newLoaders)){ $loaders = []; foreach($newLoaders as $key){ if(isset($this->fileAssociations[$key])){ $loaders[$key] = $this->fileAssociations[$key]; } } }else{ $loaders = $this->fileAssociations; } foreach($loaders as $loader){ foreach(new \DirectoryIterator($directory) as $file){ if($file === "." or $file === ".."){ continue; } $file = $directory . $file; if(!$loader->canLoadPlugin($file)){ continue; } try{ $description = $loader->getPluginDescription($file); if($description instanceof PluginDescription){ $name = $description->getName(); if(stripos($name, "pocketmine") !== false or stripos($name, "minecraft") !== false or stripos($name, "mojang") !== false){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [$name, "%pocketmine.plugin.restrictedName"])); continue; }elseif(strpos($name, " ") !== false){ $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.plugin.spacesDiscouraged", [$name])); } if(isset($plugins[$name]) or $this->getPlugin($name) instanceof Plugin){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.duplicateError", [$name])); continue; } if(!$this->isCompatibleApi(...$description->getCompatibleApis())){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [ $name, $this->server->getLanguage()->translateString("%pocketmine.plugin.incompatibleAPI", [implode(", ", $description->getCompatibleApis())]) ])); continue; } if(count($pluginMcpeProtocols = $description->getCompatibleMcpeProtocols()) > 0){ $serverMcpeProtocols = [ProtocolInfo::CURRENT_PROTOCOL]; if(count(array_intersect($pluginMcpeProtocols, $serverMcpeProtocols)) === 0){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [ $name, $this->server->getLanguage()->translateString("%pocketmine.plugin.incompatibleProtocol", [implode(", ", $pluginMcpeProtocols)]) ])); continue; } } $plugins[$name] = $file; $softDependencies[$name] = $description->getSoftDepend(); $dependencies[$name] = $description->getDepend(); foreach($description->getLoadBefore() as $before){ if(isset($softDependencies[$before])){ $softDependencies[$before][] = $name; }else{ $softDependencies[$before] = [$name]; } } } }catch(\Throwable $e){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.fileError", [$file, $directory, $e->getMessage()])); $this->server->getLogger()->logException($e); } } } while(count($plugins) > 0){ $missingDependency = true; foreach($plugins as $name => $file){ if(isset($dependencies[$name])){ foreach($dependencies[$name] as $key => $dependency){ if(isset($loadedPlugins[$dependency]) or $this->getPlugin($dependency) instanceof Plugin){ unset($dependencies[$name][$key]); }elseif(!isset($plugins[$dependency])){ $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [ $name, $this->server->getLanguage()->translateString("%pocketmine.plugin.unknownDependency", [$dependency]) ])); unset($plugins[$name]); continue 2; } } if(count($dependencies[$name]) === 0){ unset($dependencies[$name]); } } if(isset($softDependencies[$name])){ foreach($softDependencies[$name] as $key => $dependency){ if(isset($loadedPlugins[$dependency]) or $this->getPlugin($dependency) instanceof Plugin){ unset($softDependencies[$name][$key]); } } if(count($softDependencies[$name]) === 0){ unset($softDependencies[$name]); } } if(!isset($dependencies[$name]) and !isset($softDependencies[$name])){ unset($plugins[$name]); $missingDependency = false; if($plugin = $this->loadPlugin($file, $loaders) and $plugin instanceof Plugin){ $loadedPlugins[$name] = $plugin; }else{ $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.genericLoadError", [$name])); } } } if($missingDependency){ foreach($plugins as $name => $file){ if(!isset($dependencies[$name])){ unset($softDependencies[$name]); unset($plugins[$name]); $missingDependency = false; if($plugin = $this->loadPlugin($file, $loaders) and $plugin instanceof Plugin){ $loadedPlugins[$name] = $plugin; }else{ $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.genericLoadError", [$name])); } } } //No plugins loaded :( if($missingDependency){ foreach($plugins as $name => $file){ $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [$name, "%pocketmine.plugin.circularDependency"])); } $plugins = []; } } } return $loadedPlugins; }else{ return []; } } /** * Returns whether a specified API version string is considered compatible with the server's API version. * * @param string ...$versions * @return bool */ public function isCompatibleApi(string ...$versions) : bool{ $serverString = $this->server->getApiVersion(); $serverApi = array_pad(explode("-", $serverString), 2, ""); $serverNumbers = array_map("intval", explode(".", $serverApi[0])); foreach($versions as $version){ //Format: majorVersion.minorVersion.patch (3.0.0) // or: majorVersion.minorVersion.patch-devBuild (3.0.0-alpha1) if($version !== $serverString){ $pluginApi = array_pad(explode("-", $version), 2, ""); //0 = version, 1 = suffix (optional) if(strtoupper($pluginApi[1]) !== strtoupper($serverApi[1])){ //Different release phase (alpha vs. beta) or phase build (alpha.1 vs alpha.2) continue; } $pluginNumbers = array_map("intval", array_pad(explode(".", $pluginApi[0]), 3, "0")); //plugins might specify API like "3.0" or "3" if($pluginNumbers[0] !== $serverNumbers[0]){ //Completely different API version continue; } if($pluginNumbers[1] > $serverNumbers[1]){ //If the plugin requires new API features, being backwards compatible continue; } if($pluginNumbers[1] === $serverNumbers[1] and $pluginNumbers[2] > $serverNumbers[2]){ //If the plugin requires bug fixes in patches, being backwards compatible continue; } } return true; } return false; } /** * @param Plugin $plugin * * @return bool */ public function isPluginEnabled(Plugin $plugin) : bool{ return isset($this->plugins[$plugin->getDescription()->getName()]) and $plugin->isEnabled(); } /** * @param Plugin $plugin */ public function enablePlugin(Plugin $plugin){ if(!$plugin->isEnabled()){ try{ $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.plugin.enable", [$plugin->getDescription()->getFullName()])); $permManager = PermissionManager::getInstance(); foreach($plugin->getDescription()->getPermissions() as $perm){ $permManager->addPermission($perm); } $plugin->getScheduler()->setEnabled(true); $plugin->setEnabled(true); $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin; (new PluginEnableEvent($plugin))->call(); }catch(\Throwable $e){ $this->server->getLogger()->logException($e); $this->disablePlugin($plugin); } } } /** * @param Plugin $plugin * * @return PluginCommand[] */ protected function parseYamlCommands(Plugin $plugin) : array{ $pluginCmds = []; foreach($plugin->getDescription()->getCommands() as $key => $data){ if(strpos($key, ":") !== false){ $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.commandError", [$key, $plugin->getDescription()->getFullName()])); continue; } if(is_array($data)){ $newCmd = new PluginCommand($key, $plugin); if(isset($data["description"])){ $newCmd->setDescription($data["description"]); } if(isset($data["usage"])){ $newCmd->setUsage($data["usage"]); } if(isset($data["aliases"]) and is_array($data["aliases"])){ $aliasList = []; foreach($data["aliases"] as $alias){ if(strpos($alias, ":") !== false){ $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.aliasError", [$alias, $plugin->getDescription()->getFullName()])); continue; } $aliasList[] = $alias; } $newCmd->setAliases($aliasList); } if(isset($data["permission"])){ if(is_bool($data["permission"])){ $newCmd->setPermission($data["permission"] ? "true" : "false"); }elseif(is_string($data["permission"])){ $newCmd->setPermission($data["permission"]); }else{ throw new \InvalidArgumentException("Permission must be a string or boolean, " . gettype($data["permission"]) . " given"); } } if(isset($data["permission-message"])){ $newCmd->setPermissionMessage($data["permission-message"]); } $pluginCmds[] = $newCmd; } } return $pluginCmds; } public function disablePlugins(){ foreach($this->getPlugins() as $plugin){ $this->disablePlugin($plugin); } } /** * @param Plugin $plugin */ public function disablePlugin(Plugin $plugin){ if($plugin->isEnabled()){ $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.plugin.disable", [$plugin->getDescription()->getFullName()])); (new PluginDisableEvent($plugin))->call(); unset($this->enabledPlugins[$plugin->getDescription()->getName()]); try{ $plugin->setEnabled(false); }catch(\Throwable $e){ $this->server->getLogger()->logException($e); } $plugin->getScheduler()->shutdown(); HandlerList::unregisterAll($plugin); $permManager = PermissionManager::getInstance(); foreach($plugin->getDescription()->getPermissions() as $perm){ $permManager->removePermission($perm); } } } public function tickSchedulers(int $currentTick) : void{ foreach($this->enabledPlugins as $p){ $p->getScheduler()->mainThreadHeartbeat($currentTick); } } public function clearPlugins(){ $this->disablePlugins(); $this->plugins = []; $this->enabledPlugins = []; $this->fileAssociations = []; } /** * Calls an event * * @deprecated * @see Event::call() * * @param Event $event */ public function callEvent(Event $event){ $event->call(); } /** * Registers all the events in the given Listener class * * @param Listener $listener * @param Plugin $plugin * * @throws PluginException */ public function registerEvents(Listener $listener, Plugin $plugin) : void{ if(!$plugin->isEnabled()){ throw new PluginException("Plugin attempted to register " . get_class($listener) . " while not enabled"); } $reflection = new \ReflectionClass(get_class($listener)); foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method){ if(!$method->isStatic() and $method->getDeclaringClass()->implementsInterface(Listener::class)){ $tags = Utils::parseDocComment((string) $method->getDocComment()); if(isset($tags["notHandler"])){ continue; } try{ $priority = isset($tags["priority"]) ? EventPriority::fromString($tags["priority"]) : EventPriority::NORMAL; }catch(\InvalidArgumentException $e){ throw new PluginException("Event handler " . get_class($listener) . "->" . $method->getName() . "() declares invalid/unknown priority \"" . $tags["priority"] . "\""); } $ignoreCancelled = false; if(isset($tags["ignoreCancelled"])){ switch(strtolower($tags["ignoreCancelled"])){ case "true": case "": $ignoreCancelled = true; break; case "false": $ignoreCancelled = false; break; default: throw new PluginException("Event handler " . get_class($listener) . "->" . $method->getName() . "() declares invalid @ignoreCancelled value \"" . $tags["ignoreCancelled"] . "\""); } } $parameters = $method->getParameters(); try{ $isHandler = count($parameters) === 1 && $parameters[0]->getClass() instanceof \ReflectionClass && is_subclass_of($parameters[0]->getClass()->getName(), Event::class); }catch(\ReflectionException $e){ if(isset($tags["softDepend"]) && !isset($this->plugins[$tags["softDepend"]])){ $this->server->getLogger()->debug("Not registering @softDepend listener " . get_class($listener) . "::" . $method->getName() . "(" . $parameters[0]->getType()->getName() . ") because plugin \"" . $tags["softDepend"] . "\" not found"); continue; } throw $e; } if($isHandler){ $class = $parameters[0]->getClass()->getName(); $this->registerEvent($class, $listener, $priority, new MethodEventExecutor($method->getName()), $plugin, $ignoreCancelled); } } } } /** * @param string $event Class name that extends Event * @param Listener $listener * @param int $priority * @param EventExecutor $executor * @param Plugin $plugin * @param bool $ignoreCancelled * * @throws PluginException */ public function registerEvent(string $event, Listener $listener, int $priority, EventExecutor $executor, Plugin $plugin, bool $ignoreCancelled = false) : void{ if(!is_subclass_of($event, Event::class)){ throw new PluginException($event . " is not an Event"); } $tags = Utils::parseDocComment((string) (new \ReflectionClass($event))->getDocComment()); if(isset($tags["deprecated"]) and $this->server->getProperty("settings.deprecated-verbose", true)){ $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.plugin.deprecatedEvent", [ $plugin->getName(), $event, get_class($listener) . "->" . ($executor instanceof MethodEventExecutor ? $executor->getMethod() : "") ])); } if(!$plugin->isEnabled()){ throw new PluginException("Plugin attempted to register " . $event . " while not enabled"); } $timings = new TimingsHandler("Plugin: " . $plugin->getDescription()->getFullName() . " Event: " . get_class($listener) . "::" . ($executor instanceof MethodEventExecutor ? $executor->getMethod() : "???") . "(" . (new \ReflectionClass($event))->getShortName() . ")"); $this->getEventListeners($event)->register(new RegisteredListener($listener, $executor, $priority, $plugin, $ignoreCancelled, $timings)); } /** * @param string $event * * @return HandlerList */ private function getEventListeners(string $event) : HandlerList{ $list = HandlerList::getHandlerListFor($event); if($list === null){ throw new PluginException("Abstract events not declaring @allowHandle cannot be handled (tried to register listener for $event)"); } return $list; } }