76    protected array $plugins = [];
 
   82    protected array $enabledPlugins = [];
 
   85    private array $pluginDependents = [];
 
   87    private bool $loadPluginsGuard = 
false;
 
   93    protected array $fileAssociations = [];
 
   95    public function __construct(
 
   97        private ?
string $pluginDataDirectory,
 
  100        if($this->pluginDataDirectory !== 
null){
 
  101            if(!file_exists($this->pluginDataDirectory)){
 
  102                @mkdir($this->pluginDataDirectory, 0777, 
true);
 
  103            }elseif(!is_dir($this->pluginDataDirectory)){
 
  104                throw new \RuntimeException(
"Plugin data path $this->pluginDataDirectory exists and is not a directory");
 
  109    public function getPlugin(
string $name) : ?
Plugin{
 
  110        if(isset($this->plugins[$name])){
 
  111            return $this->plugins[$name];
 
  117    public function registerInterface(
PluginLoader $loader) : 
void{
 
  118        $this->fileAssociations[get_class($loader)] = $loader;
 
  126        return $this->plugins;
 
 
  129    private function getDataDirectory(
string $pluginPath, 
string $pluginName) : string{
 
  130        if($this->pluginDataDirectory !== null){
 
  131            return Path::join($this->pluginDataDirectory, $pluginName);
 
  133        return Path::join(dirname($pluginPath), $pluginName);
 
  136    private function internalLoadPlugin(
string $path, PluginLoader $loader, PluginDescription $description) : ?Plugin{
 
  137        $language = $this->server->getLanguage();
 
  138        $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_load($description->getFullName())));
 
  140        $dataFolder = $this->getDataDirectory($path, $description->getName());
 
  141        if(file_exists($dataFolder) && !is_dir($dataFolder)){
 
  142            $this->server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  143                $description->getName(),
 
  144                KnownTranslationFactory::pocketmine_plugin_badDataFolder($dataFolder)
 
  148        if(!file_exists($dataFolder)){
 
  149            mkdir($dataFolder, 0777, 
true);
 
  152        $prefixed = $loader->getAccessProtocol() . $path;
 
  153        $loader->loadPlugin($prefixed);
 
  155        $mainClass = $description->getMain();
 
  156        if(!class_exists($mainClass, 
true)){
 
  157            $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  158                $description->getName(),
 
  159                KnownTranslationFactory::pocketmine_plugin_mainClassNotFound()
 
  163        if(!is_a($mainClass, Plugin::class, 
true)){
 
  164            $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  165                $description->getName(),
 
  166                KnownTranslationFactory::pocketmine_plugin_mainClassWrongType(Plugin::class)
 
  170        $reflect = new \ReflectionClass($mainClass); 
 
  171        if(!$reflect->isInstantiable()){
 
  172            $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  173                $description->getName(),
 
  174                KnownTranslationFactory::pocketmine_plugin_mainClassAbstract()
 
  179        $permManager = PermissionManager::getInstance();
 
  180        foreach($description->getPermissions() as $permsGroup){
 
  181            foreach($permsGroup as $perm){
 
  182                if($permManager->getPermission($perm->getName()) !== 
null){
 
  183                    $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  184                        $description->getName(),
 
  185                        KnownTranslationFactory::pocketmine_plugin_duplicatePermissionError($perm->getName())
 
  191        $opRoot = $permManager->getPermission(DefaultPermissions::ROOT_OPERATOR);
 
  192        $everyoneRoot = $permManager->getPermission(DefaultPermissions::ROOT_USER);
 
  193        foreach(Utils::stringifyKeys($description->getPermissions()) as $default => $perms){
 
  194            foreach($perms as $perm){
 
  195                $permManager->addPermission($perm);
 
  197                    case PermissionParser::DEFAULT_TRUE:
 
  198                        $everyoneRoot->addChild($perm->getName(), 
true);
 
  200                    case PermissionParser::DEFAULT_OP:
 
  201                        $opRoot->addChild($perm->getName(), 
true);
 
  203                    case PermissionParser::DEFAULT_NOT_OP:
 
  210                        $everyoneRoot->addChild($perm->getName(), 
true);
 
  211                        $opRoot->addChild($perm->getName(), 
false);
 
  223        $plugin = 
new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed, 
new DiskResourceProvider($prefixed . 
"/resources/"));
 
  224        $this->plugins[$plugin->getDescription()->getName()] = $plugin;
 
  233    private function triagePlugins(
string $path, PluginLoadTriage $triage, 
int &$loadErrorCount, ?array $newLoaders = 
null) : void{
 
  234        if(is_array($newLoaders)){
 
  236            foreach($newLoaders as $key){
 
  237                if(isset($this->fileAssociations[$key])){
 
  238                    $loaders[$key] = $this->fileAssociations[$key];
 
  242            $loaders = $this->fileAssociations;
 
  246            $files = iterator_to_array(
new \FilesystemIterator($path, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
 
  248        }elseif(is_file($path)){
 
  249            $realPath = Utils::assumeNotFalse(realpath($path), 
"realpath() should not return false on an accessible, existing file");
 
  250            $files = [$realPath];
 
  255        $loadabilityChecker = 
new PluginLoadabilityChecker($this->
server->getApiVersion());
 
  256        foreach($loaders as $loader){
 
  257            foreach($files as $file){
 
  258                if(!is_string($file)) 
throw new AssumptionFailedError(
"FilesystemIterator current should be string when using CURRENT_AS_PATHNAME");
 
  259                if(!$loader->canLoadPlugin($file)){
 
  263                    $description = $loader->getPluginDescription($file);
 
  264                }
catch(PluginDescriptionParseException $e){
 
  265                    $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  267                        KnownTranslationFactory::pocketmine_plugin_invalidManifest($e->getMessage())
 
  271                }
catch(\RuntimeException $e){ 
 
  272                    $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($file, $e->getMessage())));
 
  273                    $this->
server->getLogger()->logException($e);
 
  277                if($description === 
null){
 
  281                $name = $description->getName();
 
  283                if($this->graylist !== 
null && !$this->graylist->isAllowed($name)){
 
  284                    $this->
server->getLogger()->notice($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  286                        $this->graylist->isWhitelist() ? KnownTranslationFactory::pocketmine_plugin_disallowedByWhitelist() : KnownTranslationFactory::pocketmine_plugin_disallowedByBlacklist()
 
  294                if(($loadabilityError = $loadabilityChecker->check($description)) !== 
null){
 
  295                    $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, $loadabilityError)));
 
  300                if(isset($triage->plugins[$name]) || $this->getPlugin($name) instanceof Plugin){
 
  301                    $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_duplicateError($name)));
 
  306                if(str_contains($name, 
" ")){
 
  307                    $this->
server->getLogger()->warning($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name)));
 
  310                $triage->plugins[$name] = 
new PluginLoadTriageEntry($file, $loader, $description);
 
  312                $triage->softDependencies[$name] = array_merge($triage->softDependencies[$name] ?? [], $description->getSoftDepend());
 
  313                $triage->dependencies[$name] = $description->getDepend();
 
  315                foreach($description->getLoadBefore() as $before){
 
  316                    if(isset($triage->softDependencies[$before])){
 
  317                        $triage->softDependencies[$before][] = $name;
 
  319                        $triage->softDependencies[$before] = [$name];
 
  333    private function checkDepsForTriage(
string $pluginName, 
string $dependencyType, array &$dependencyLists, array $loadedPlugins, PluginLoadTriage $triage) : void{
 
  334        if(isset($dependencyLists[$pluginName])){
 
  335            foreach(Utils::promoteKeys($dependencyLists[$pluginName]) as $key => $dependency){
 
  336                if(isset($loadedPlugins[$dependency]) || $this->getPlugin($dependency) instanceof Plugin){
 
  337                    $this->
server->getLogger()->debug(
"Successfully resolved $dependencyType dependency \"$dependency\" for plugin \"$pluginName\"");
 
  338                    unset($dependencyLists[$pluginName][$key]);
 
  339                }elseif(array_key_exists($dependency, $triage->plugins)){
 
  340                    $this->
server->getLogger()->debug(
"Deferring resolution of $dependencyType dependency \"$dependency\" for plugin \"$pluginName\" (found but not loaded yet)");
 
  344            if(count($dependencyLists[$pluginName]) === 0){
 
  345                unset($dependencyLists[$pluginName]);
 
  353    public function loadPlugins(
string $path, 
int &$loadErrorCount = 0) : array{
 
  354        if($this->loadPluginsGuard){
 
  355            throw new \LogicException(__METHOD__ . 
"() cannot be called from within itself");
 
  357        $this->loadPluginsGuard = 
true;
 
  360        $this->triagePlugins($path, $triage, $loadErrorCount);
 
  364        while(count($triage->plugins) > 0){
 
  366            foreach(Utils::stringifyKeys($triage->plugins) as $name => $entry){
 
  367                $this->checkDepsForTriage($name, 
"hard", $triage->dependencies, $loadedPlugins, $triage);
 
  368                $this->checkDepsForTriage($name, 
"soft", $triage->softDependencies, $loadedPlugins, $triage);
 
  370                if(!isset($triage->dependencies[$name]) && !isset($triage->softDependencies[$name])){
 
  371                    unset($triage->plugins[$name]);
 
  374                    $oldRegisteredLoaders = $this->fileAssociations;
 
  375                    if(($plugin = $this->internalLoadPlugin($entry->getFile(), $entry->getLoader(), $entry->getDescription())) instanceof 
Plugin){
 
  376                        $loadedPlugins[$name] = $plugin;
 
  378                        foreach($this->fileAssociations as $k => $loader){
 
  379                            if(!array_key_exists($k, $oldRegisteredLoaders)){
 
  383                        if(count($diffLoaders) !== 0){
 
  384                            $this->server->getLogger()->debug(
"Plugin $name registered a new plugin loader during load, scanning for new plugins");
 
  385                            $plugins = $triage->plugins;
 
  386                            $this->triagePlugins($path, $triage, $loadErrorCount, $diffLoaders);
 
  387                            $diffPlugins = array_diff_key($triage->plugins, $plugins);
 
  388                            $this->server->getLogger()->debug(
"Re-triage found plugins: " . implode(
", ", array_keys($diffPlugins)));
 
  396            if($loadedThisLoop === 0){
 
  400                foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
 
  401                    if(isset($triage->softDependencies[$name]) && !isset($triage->dependencies[$name])){
 
  402                        foreach(Utils::promoteKeys($triage->softDependencies[$name]) as $k => $dependency){
 
  403                            if($this->getPlugin($dependency) === 
null && !array_key_exists($dependency, $triage->plugins)){
 
  404                                $this->
server->getLogger()->debug(
"Skipping resolution of missing soft dependency \"$dependency\" for plugin \"$name\"");
 
  405                                unset($triage->softDependencies[$name][$k]);
 
  408                        if(count($triage->softDependencies[$name]) === 0){
 
  409                            unset($triage->softDependencies[$name]);
 
  415                foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
 
  416                    if(isset($triage->dependencies[$name])){
 
  417                        $unknownDependencies = [];
 
  419                        foreach($triage->dependencies[$name] as $dependency){
 
  420                            if($this->getPlugin($dependency) === 
null && !array_key_exists($dependency, $triage->plugins)){
 
  424                                $unknownDependencies[$dependency] = $dependency;
 
  428                        if(count($unknownDependencies) > 0){
 
  429                            $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
 
  431                                KnownTranslationFactory::pocketmine_plugin_unknownDependency(implode(
", ", $unknownDependencies))
 
  433                            unset($triage->plugins[$name]);
 
  439                foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
 
  440                    $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, KnownTranslationFactory::pocketmine_plugin_circularDependency())));
 
  447        $this->loadPluginsGuard = 
false;
 
  448        return $loadedPlugins;
 
 
  451    public function isPluginEnabled(Plugin $plugin) : bool{
 
  452        return isset($this->plugins[$plugin->getDescription()->getName()]) && $plugin->isEnabled();
 
  455    public function enablePlugin(Plugin $plugin) : bool{
 
  456        if(!$plugin->isEnabled()){
 
  457            $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_enable($plugin->getDescription()->getFullName())));
 
  459            $plugin->getScheduler()->setEnabled(
true);
 
  461                $plugin->onEnableStateChange(true);
 
  462            }
catch(DisablePluginException){
 
  463                $this->disablePlugin($plugin);
 
  466            if($plugin->isEnabled()){ 
 
  467                $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin;
 
  469                foreach($plugin->getDescription()->getDepend() as $dependency){
 
  470                    $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
 
  472                foreach($plugin->getDescription()->getSoftDepend() as $dependency){
 
  473                    if(isset($this->plugins[$dependency])){
 
  474                        $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
 
  478                (
new PluginEnableEvent($plugin))->call();
 
  482                $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(
 
  483                    KnownTranslationFactory::pocketmine_plugin_enableError(
 
  485                        KnownTranslationFactory::pocketmine_plugin_suicide()
 
  496    public function disablePlugins() : void{
 
  497        while(count($this->enabledPlugins) > 0){
 
  498            foreach($this->enabledPlugins as $plugin){
 
  499                if(!$plugin->isEnabled()){
 
  502                $name = $plugin->getDescription()->getName();
 
  503                if(isset($this->pluginDependents[$name]) && count($this->pluginDependents[$name]) > 0){
 
  504                    $this->
server->getLogger()->debug(
"Deferring disable of plugin $name due to dependent plugins still enabled: " . implode(
", ", array_keys($this->pluginDependents[$name])));
 
  508                $this->disablePlugin($plugin);
 
  513    public function disablePlugin(Plugin $plugin) : void{
 
  514        if($plugin->isEnabled()){
 
  515            $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_disable($plugin->getDescription()->getFullName())));
 
  516            (
new PluginDisableEvent($plugin))->call();
 
  518            unset($this->enabledPlugins[$plugin->getDescription()->getName()]);
 
  519            foreach(Utils::stringifyKeys($this->pluginDependents) as $dependency => $dependentList){
 
  520                if(isset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()])){
 
  521                    if(count($this->pluginDependents[$dependency]) === 1){
 
  522                        unset($this->pluginDependents[$dependency]);
 
  524                        unset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()]);
 
  529            $plugin->onEnableStateChange(
false);
 
  530            $plugin->getScheduler()->shutdown();
 
  531            HandlerListManager::global()->unregisterAll($plugin);
 
  535    public function tickSchedulers(
int $currentTick) : void{
 
  536        foreach(Utils::promoteKeys($this->enabledPlugins) as $pluginName => $p){
 
  537            if(isset($this->enabledPlugins[$pluginName])){
 
  540                $p->getScheduler()->mainThreadHeartbeat($currentTick);
 
  545    public function clearPlugins() : void{
 
  546        $this->disablePlugins();
 
  548        $this->enabledPlugins = [];
 
  549        $this->fileAssociations = [];
 
  562    private function getEventsHandledBy(\ReflectionMethod $method) : ?string{
 
  563        if($method->isStatic() || !$method->getDeclaringClass()->implementsInterface(Listener::class)){
 
  566        $tags = Utils::parseDocComment((
string) $method->getDocComment());
 
  567        if(isset($tags[ListenerMethodTags::NOT_HANDLER])){
 
  571        $parameters = $method->getParameters();
 
  572        if(count($parameters) !== 1){
 
  576        $paramType = $parameters[0]->getType();
 
  578        if(!$paramType instanceof \ReflectionNamedType || $paramType->isBuiltin()){
 
  583        $paramClass = $paramType->getName();
 
  584        $eventClass = new \ReflectionClass($paramClass);
 
  585        if(!$eventClass->isSubclassOf(Event::class)){
 
  590        return $eventClass->getName();
 
  599        if(!$plugin->isEnabled()){
 
  600            throw new PluginException(
"Plugin attempted to register " . get_class($listener) . 
" while not enabled");
 
  603        $reflection = new \ReflectionClass(get_class($listener));
 
  604        foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method){
 
  605            $tags = Utils::parseDocComment((
string) $method->getDocComment());
 
  606            if(isset($tags[ListenerMethodTags::NOT_HANDLER]) || ($eventClass = $this->getEventsHandledBy($method)) === 
null){
 
  609            $handlerClosure = $method->getClosure($listener);
 
  613                $priority = isset($tags[ListenerMethodTags::PRIORITY]) ? EventPriority::fromString($tags[ListenerMethodTags::PRIORITY]) : EventPriority::NORMAL;
 
  614            }
catch(\InvalidArgumentException $e){
 
  615                throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) . 
"() declares invalid/unknown priority \"" . $tags[ListenerMethodTags::PRIORITY] . 
"\"");
 
  618            $handleCancelled = 
false;
 
  619            if(isset($tags[ListenerMethodTags::HANDLE_CANCELLED])){
 
  620                if(!is_a($eventClass, Cancellable::class, 
true)){
 
  621                    throw new PluginException(sprintf(
 
  622                        "Event handler %s() declares @%s for non-cancellable event of type %s",
 
  623                        Utils::getNiceClosureName($handlerClosure),
 
  624                        ListenerMethodTags::HANDLE_CANCELLED,
 
  628                switch(strtolower($tags[ListenerMethodTags::HANDLE_CANCELLED])){
 
  631                        $handleCancelled = 
true;
 
  636                        throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) . 
"() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED . 
" value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] . 
"\"");
 
  640            $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
 
 
  654        if(!is_subclass_of($event, 
Event::class)){
 
  658        $handlerName = Utils::getNiceClosureName($handler);
 
  660        $reflect = new \ReflectionFunction($handler);
 
  661        if($reflect->isGenerator()){
 
  662            throw new PluginException(
"Generator function $handlerName cannot be used as an event handler");
 
  665        if(!$plugin->isEnabled()){
 
  666            throw new PluginException(
"Plugin attempted to register event handler " . $handlerName . 
"() to event " . $event . 
" while not enabled");
 
  669        $timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
 
  671        $registeredListener = 
new RegisteredListener($handler, $priority, $plugin, $handleCancelled, $timings);
 
  672        HandlerListManager::global()->getListFor($event)->register($registeredListener);
 
  673        return $registeredListener;