PocketMine-MP 5.23.3 git-f7687af337d001ddbcc47b8e773f014a33faa662
Loading...
Searching...
No Matches
PluginManager.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\plugin;
25
43use Symfony\Component\Filesystem\Path;
44use function array_diff_key;
45use function array_key_exists;
46use function array_keys;
47use function array_merge;
48use function class_exists;
49use function count;
50use function dirname;
51use function file_exists;
52use function get_class;
53use function implode;
54use function is_a;
55use function is_array;
56use function is_dir;
57use function is_file;
58use function is_string;
59use function is_subclass_of;
60use function iterator_to_array;
61use function mkdir;
62use function realpath;
63use function shuffle;
64use function sprintf;
65use function str_contains;
66use function strtolower;
67
76 protected array $plugins = [];
77
82 protected array $enabledPlugins = [];
83
85 private array $pluginDependents = [];
86
87 private bool $loadPluginsGuard = false;
88
93 protected array $fileAssociations = [];
94
95 public function __construct(
96 private Server $server,
97 private ?string $pluginDataDirectory,
98 private ?PluginGraylist $graylist = null
99 ){
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");
105 }
106 }
107 }
108
109 public function getPlugin(string $name) : ?Plugin{
110 if(isset($this->plugins[$name])){
111 return $this->plugins[$name];
112 }
113
114 return null;
115 }
116
117 public function registerInterface(PluginLoader $loader) : void{
118 $this->fileAssociations[get_class($loader)] = $loader;
119 }
120
125 public function getPlugins() : array{
126 return $this->plugins;
127 }
128
129 private function getDataDirectory(string $pluginPath, string $pluginName) : string{
130 if($this->pluginDataDirectory !== null){
131 return Path::join($this->pluginDataDirectory, $pluginName);
132 }
133 return Path::join(dirname($pluginPath), $pluginName);
134 }
135
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())));
139
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)
145 )));
146 return null;
147 }
148 if(!file_exists($dataFolder)){
149 mkdir($dataFolder, 0777, true);
150 }
151
152 $prefixed = $loader->getAccessProtocol() . $path;
153 $loader->loadPlugin($prefixed);
154
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()
160 )));
161 return null;
162 }
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)
167 )));
168 return null;
169 }
170 $reflect = new \ReflectionClass($mainClass); //this shouldn't throw; we already checked that it exists
171 if(!$reflect->isInstantiable()){
172 $this->server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
173 $description->getName(),
174 KnownTranslationFactory::pocketmine_plugin_mainClassAbstract()
175 )));
176 return null;
177 }
178
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())
186 )));
187 return null;
188 }
189 }
190 }
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);
196 switch($default){
197 case PermissionParser::DEFAULT_TRUE:
198 $everyoneRoot->addChild($perm->getName(), true);
199 break;
200 case PermissionParser::DEFAULT_OP:
201 $opRoot->addChild($perm->getName(), true);
202 break;
203 case PermissionParser::DEFAULT_NOT_OP:
204 //TODO: I don't think anyone uses this, and it currently relies on some magic inside PermissibleBase
205 //to ensure that the operator override actually applies.
206 //Explore getting rid of this.
207 //The following grants this permission to anyone who has the "everyone" root permission.
208 //However, if the operator root node (which has higher priority) is present, the
209 //permission will be denied instead.
210 $everyoneRoot->addChild($perm->getName(), true);
211 $opRoot->addChild($perm->getName(), false);
212 break;
213 default:
214 break;
215 }
216 }
217 }
218
223 $plugin = new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed, new DiskResourceProvider($prefixed . "/resources/"));
224 $this->plugins[$plugin->getDescription()->getName()] = $plugin;
225
226 return $plugin;
227 }
228
233 private function triagePlugins(string $path, PluginLoadTriage $triage, int &$loadErrorCount, ?array $newLoaders = null) : void{
234 if(is_array($newLoaders)){
235 $loaders = [];
236 foreach($newLoaders as $key){
237 if(isset($this->fileAssociations[$key])){
238 $loaders[$key] = $this->fileAssociations[$key];
239 }
240 }
241 }else{
242 $loaders = $this->fileAssociations;
243 }
244
245 if(is_dir($path)){
246 $files = iterator_to_array(new \FilesystemIterator($path, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
247 shuffle($files); //this prevents plugins implicitly relying on the filesystem name order when they should be using dependency properties
248 }elseif(is_file($path)){
249 $realPath = Utils::assumeNotFalse(realpath($path), "realpath() should not return false on an accessible, existing file");
250 $files = [$realPath];
251 }else{
252 return;
253 }
254
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)){
260 continue;
261 }
262 try{
263 $description = $loader->getPluginDescription($file);
264 }catch(PluginDescriptionParseException $e){
265 $this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
266 $file,
267 KnownTranslationFactory::pocketmine_plugin_invalidManifest($e->getMessage())
268 )));
269 $loadErrorCount++;
270 continue;
271 }catch(\RuntimeException $e){ //TODO: more specific exception handling
272 $this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($file, $e->getMessage())));
273 $this->server->getLogger()->logException($e);
274 $loadErrorCount++;
275 continue;
276 }
277 if($description === null){
278 continue;
279 }
280
281 $name = $description->getName();
282
283 if($this->graylist !== null && !$this->graylist->isAllowed($name)){
284 $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
285 $name,
286 $this->graylist->isWhitelist() ? KnownTranslationFactory::pocketmine_plugin_disallowedByWhitelist() : KnownTranslationFactory::pocketmine_plugin_disallowedByBlacklist()
287 )));
288 //this does NOT increment loadErrorCount, because using the graylist to prevent a plugin from
289 //loading is not considered accidental; this is the same as if the plugin were manually removed
290 //this means that the server will continue to boot even if some plugins were blocked by graylist
291 continue;
292 }
293
294 if(($loadabilityError = $loadabilityChecker->check($description)) !== null){
295 $this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, $loadabilityError)));
296 $loadErrorCount++;
297 continue;
298 }
299
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)));
302 $loadErrorCount++;
303 continue;
304 }
305
306 if(str_contains($name, " ")){
307 $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name)));
308 }
309
310 $triage->plugins[$name] = new PluginLoadTriageEntry($file, $loader, $description);
311
312 $triage->softDependencies[$name] = array_merge($triage->softDependencies[$name] ?? [], $description->getSoftDepend());
313 $triage->dependencies[$name] = $description->getDepend();
314
315 foreach($description->getLoadBefore() as $before){
316 if(isset($triage->softDependencies[$before])){
317 $triage->softDependencies[$before][] = $name;
318 }else{
319 $triage->softDependencies[$before] = [$name];
320 }
321 }
322 }
323 }
324 }
325
333 private function checkDepsForTriage(string $pluginName, string $dependencyType, array &$dependencyLists, array $loadedPlugins, PluginLoadTriage $triage) : void{
334 if(isset($dependencyLists[$pluginName])){
335 foreach($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)");
341 }
342 }
343
344 if(count($dependencyLists[$pluginName]) === 0){
345 unset($dependencyLists[$pluginName]);
346 }
347 }
348 }
349
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");
356 }
357 $this->loadPluginsGuard = true;
358
359 $triage = new PluginLoadTriage();
360 $this->triagePlugins($path, $triage, $loadErrorCount);
361
362 $loadedPlugins = [];
363
364 while(count($triage->plugins) > 0){
365 $loadedThisLoop = 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);
369
370 if(!isset($triage->dependencies[$name]) && !isset($triage->softDependencies[$name])){
371 unset($triage->plugins[$name]);
372 $loadedThisLoop++;
373
374 $oldRegisteredLoaders = $this->fileAssociations;
375 if(($plugin = $this->internalLoadPlugin($entry->getFile(), $entry->getLoader(), $entry->getDescription())) instanceof Plugin){
376 $loadedPlugins[$name] = $plugin;
377 $diffLoaders = [];
378 foreach($this->fileAssociations as $k => $loader){
379 if(!array_key_exists($k, $oldRegisteredLoaders)){
380 $diffLoaders[] = $k;
381 }
382 }
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)));
389 }
390 }else{
391 $loadErrorCount++;
392 }
393 }
394 }
395
396 if($loadedThisLoop === 0){
397 //No plugins loaded :(
398
399 //check for skippable soft dependencies first, in case the dependents could resolve hard dependencies
400 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
401 if(isset($triage->softDependencies[$name]) && !isset($triage->dependencies[$name])){
402 foreach($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]);
406 }
407 }
408 if(count($triage->softDependencies[$name]) === 0){
409 unset($triage->softDependencies[$name]);
410 continue 2; //go back to the top and try again
411 }
412 }
413 }
414
415 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
416 if(isset($triage->dependencies[$name])){
417 $unknownDependencies = [];
418
419 foreach($triage->dependencies[$name] as $k => $dependency){
420 if($this->getPlugin($dependency) === null && !array_key_exists($dependency, $triage->plugins)){
421 //assume that the plugin is never going to be loaded
422 //by this point all soft dependencies have been ignored if they were able to be, so
423 //there's no chance of this dependency ever being resolved
424 $unknownDependencies[$dependency] = $dependency;
425 }
426 }
427
428 if(count($unknownDependencies) > 0){
429 $this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
430 $name,
431 KnownTranslationFactory::pocketmine_plugin_unknownDependency(implode(", ", $unknownDependencies))
432 )));
433 unset($triage->plugins[$name]);
434 $loadErrorCount++;
435 }
436 }
437 }
438
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())));
441 $loadErrorCount++;
442 }
443 break;
444 }
445 }
446
447 $this->loadPluginsGuard = false;
448 return $loadedPlugins;
449 }
450
451 public function isPluginEnabled(Plugin $plugin) : bool{
452 return isset($this->plugins[$plugin->getDescription()->getName()]) && $plugin->isEnabled();
453 }
454
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())));
458
459 $plugin->getScheduler()->setEnabled(true);
460 try{
461 $plugin->onEnableStateChange(true);
462 }catch(DisablePluginException){
463 $this->disablePlugin($plugin);
464 }
465
466 if($plugin->isEnabled()){ //the plugin may have disabled itself during onEnable()
467 $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin;
468
469 foreach($plugin->getDescription()->getDepend() as $dependency){
470 $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
471 }
472 foreach($plugin->getDescription()->getSoftDepend() as $dependency){
473 if(isset($this->plugins[$dependency])){
474 $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
475 }
476 }
477
478 (new PluginEnableEvent($plugin))->call();
479
480 return true;
481 }else{
482 $this->server->getLogger()->critical($this->server->getLanguage()->translate(
483 KnownTranslationFactory::pocketmine_plugin_enableError(
484 $plugin->getName(),
485 KnownTranslationFactory::pocketmine_plugin_suicide()
486 )
487 ));
488
489 return false;
490 }
491 }
492
493 return true; //TODO: maybe this should be an error?
494 }
495
496 public function disablePlugins() : void{
497 while(count($this->enabledPlugins) > 0){
498 foreach($this->enabledPlugins as $plugin){
499 if(!$plugin->isEnabled()){
500 continue; //in case a plugin disabled another plugin
501 }
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])));
505 continue;
506 }
507
508 $this->disablePlugin($plugin);
509 }
510 }
511 }
512
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();
517
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]);
523 }else{
524 unset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()]);
525 }
526 }
527 }
528
529 $plugin->onEnableStateChange(false);
530 $plugin->getScheduler()->shutdown();
531 HandlerListManager::global()->unregisterAll($plugin);
532 }
533 }
534
535 public function tickSchedulers(int $currentTick) : void{
536 foreach(Utils::promoteKeys($this->enabledPlugins) as $pluginName => $p){
537 if(isset($this->enabledPlugins[$pluginName])){
538 //the plugin may have been disabled as a result of updating other plugins' schedulers, and therefore
539 //removed from enabledPlugins; however, foreach will still see it due to copy-on-write
540 $p->getScheduler()->mainThreadHeartbeat($currentTick);
541 }
542 }
543 }
544
545 public function clearPlugins() : void{
546 $this->disablePlugins();
547 $this->plugins = [];
548 $this->enabledPlugins = [];
549 $this->fileAssociations = [];
550 }
551
562 private function getEventsHandledBy(\ReflectionMethod $method) : ?string{
563 if($method->isStatic() || !$method->getDeclaringClass()->implementsInterface(Listener::class)){
564 return null;
565 }
566 $tags = Utils::parseDocComment((string) $method->getDocComment());
567 if(isset($tags[ListenerMethodTags::NOT_HANDLER])){
568 return null;
569 }
570
571 $parameters = $method->getParameters();
572 if(count($parameters) !== 1){
573 return null;
574 }
575
576 $paramType = $parameters[0]->getType();
577 //isBuiltin() returns false for builtin classes ..................
578 if(!$paramType instanceof \ReflectionNamedType || $paramType->isBuiltin()){
579 return null;
580 }
581
583 $paramClass = $paramType->getName();
584 $eventClass = new \ReflectionClass($paramClass);
585 if(!$eventClass->isSubclassOf(Event::class)){
586 return null;
587 }
588
590 return $eventClass->getName();
591 }
592
598 public function registerEvents(Listener $listener, Plugin $plugin) : void{
599 if(!$plugin->isEnabled()){
600 throw new PluginException("Plugin attempted to register " . get_class($listener) . " while not enabled");
601 }
602
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){
607 continue;
608 }
609 $handlerClosure = $method->getClosure($listener);
610 if($handlerClosure === null) throw new AssumptionFailedError("This should never happen");
611
612 try{
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] . "\"");
616 }
617
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,
625 $eventClass
626 ));
627 }
628 switch(strtolower($tags[ListenerMethodTags::HANDLE_CANCELLED])){
629 case "true":
630 case "":
631 $handleCancelled = true;
632 break;
633 case "false":
634 break;
635 default:
636 throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED . " value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] . "\"");
637 }
638 }
639
640 $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
641 }
642 }
643
653 public function registerEvent(string $event, \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled = false) : RegisteredListener{
654 if(!is_subclass_of($event, Event::class)){
655 throw new PluginException($event . " is not an Event");
656 }
657
658 $handlerName = Utils::getNiceClosureName($handler);
659
660 $reflect = new \ReflectionFunction($handler);
661 if($reflect->isGenerator()){
662 throw new PluginException("Generator function $handlerName cannot be used as an event handler");
663 }
664
665 if(!$plugin->isEnabled()){
666 throw new PluginException("Plugin attempted to register event handler " . $handlerName . "() to event " . $event . " while not enabled");
667 }
668
669 $timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
670
671 $registeredListener = new RegisteredListener($handler, $priority, $plugin, $handleCancelled, $timings);
672 HandlerListManager::global()->getListFor($event)->register($registeredListener);
673 return $registeredListener;
674 }
675}
registerEvent(string $event, \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled=false)
loadPlugins(string $path, int &$loadErrorCount=0)
registerEvents(Listener $listener, Plugin $plugin)