Как сделать, чтобы плагины PMMP3(api: 3.0.0) PMMP4(api: 4.0.0) [PHP7-PHP8] - работали вместе без ошибок?
Для этого потребуется дополнительный плагин или нужно (переписывать) ядро?
на стоковом пм никак
апи 4 кардинально отличается от 3
Существуют ядра на апи 3.0.0 с поддержкой новых версий
переписывать плагины на новый апи
Если ты сможешь объединить эти два апи, это будет большое открытие
переведу на человеческий: будет клоунадой
Напишу скрипт-конвертер плагинов пм3 на пм4. Пишите в лс в ВК @dickbtw
на гитхабе опенсорс есть
Уже есть такой
@MrHoller @mopsikzloS если не трудно можете дать ссылку?
сейчас найду скину.
Convert.php (16.9 КБ)
держи, но ты должен понимать, что что-то крупное лучше переписать самому, во избежание всяких разных приколов
@mopsikzloS Может сделаеш тутор на форуме как использовать конвертер?
Там же все вроде элементарно, попробуй сам скачать, запустить, потестить
На
<?php
/**
* This script converts PocketMine-MP plugins with API3.x.x to API4.x.x
* There are possible bugs, converting a plugin requires full testing after
*
* Please report any bugs you encounter while converting / testing (that are obviously caused by this script)
*
* 2022 - HimmelKreis4865
*/
$t = microtime(true) * 1000;
set_exception_handler(function ($exception): void {
log_error($exception->getMessage());
exit;
});
const IMPORT_REMAPS = [
'pocketmine\Player' => 'pocketmine\player\Player',
'pocketmine\OfflinePlayer' => 'pocketmine\player\OfflinePlayer',
'pocketmine\IPlayer' => 'pocketmine\player\IPlayer',
'pocketmine\tile' => 'pocketmine\block\tile',
'pocketmine\effect\Effect' => 'pocketmine\entity\effect\Effect',
'pocketmine\effect\EffectInstance' => 'pocketmine\entity\effect\EffectInstance',
'pocketmine\entity\DataPropertyManager' => 'pocketmine\network\mcpe\protocol\types\entity\DataPropertyManager',
'pocketmine\inventory\AnvilInventory' => 'pocketmine\block\inventory\AnvilInventory',
'pocketmine\inventory\ChestInventory' => 'pocketmine\block\inventory\ChestInventory',
'pocketmine\inventory\DoubleChestInventory' => 'pocketmine\block\inventory\DoubleChestInventory',
'pocketmine\inventory\EnchantInventory' => 'pocketmine\block\inventory\EnchantInventory',
'pocketmine\inventory\EnderChestInventory' => 'pocketmine\block\inventory\EnderChestInventory',
'pocketmine\inventory\FurnaceInventory' => 'pocketmine\block\inventory\FurnaceInventory',
'pocketmine\inventory\CraftingGrid' => 'pocketmine\crafting\CraftingGrid',
'pocketmine\inventory\CraftingManager' => 'pocketmine\crafting\CraftingManager',
'pocketmine\inventory\CraftingRecipe' => 'pocketmine\crafting\CraftingRecipe',
'pocketmine\inventory\ShapedRecipe' => 'pocketmine\crafting\ShapedRecipe',
'pocketmine\inventory\ShapelessRecipe' => 'pocketmine\crafting\ShapelessRecipe',
'pocketmine\inventory\FurnaceRecipe' => 'pocketmine\crafting\FurnaceRecipe',
'pocketmine\level\Location' => 'pocketmine\entity\Location',
'pocketmine\level\Level' => 'pocketmine\world\World',
'pocketmine\level' => 'pocketmine\world',
'pocketmine\command\PluginIdentifiableCommand' => 'pocketmine\plugin\PluginOwned',
'pocketmine\event\level' => 'pocketmine\event\world',
];
const REMAPS = [
'/(public\sfunction\sonLoad\(\))\s*[:\s]*[^\{]*/i' => 'protected function onLoad(): void',
'/(public\sfunction\sonEnable\(\))\s*[:\s]*[^\{]*/i' => 'protected function onEnable(): void',
'/(public\sfunction\sonDisable\(\))\s*[:\s]*[^\{]*/i' => 'protected function onDisable(): void',
'/(->getServer\(\)|Server::getInstance\(\))(->findEntity\()/i' => '$1->getWorldManager()$2',
'/(->getServer\(\)|Server::getInstance\(\))(->generateLevel\()/i' => '$1->getWorldManager()->generateWorld(',
'/(->getServer\(\)|Server::getInstance\(\))(->getAutoSave\()/i' => '$1->getWorldManager()$2',
'/(->getServer\(\)|Server::getInstance\(\))(->setAutoSave\()/i' => '$1->getWorldManager()$2',
'/(->getServer\(\)|Server::getInstance\(\))(->getDefaultLevel\()/i' => '$1->getWorldManager()->getDefaultWorld(',
'/(->getServer\(\)|Server::getInstance\(\))(->getLevel\()/i' => '$1->getWorldManager()->getWorld(',
'/(->getServer\(\)|Server::getInstance\(\))(->getLevelByName\()/i' => '$1->getWorldManager()->getWorldByName(',
'/(->getServer\(\)|Server::getInstance\(\))(->getLevels\()/i' => '$1->getWorldManager()->getWorlds(',
'/(->getServer\(\)|Server::getInstance\(\))(->isLevelGenerated\()/i' => '$1->getWorldManager()->isWorldGenerated(',
'/(->getServer\(\)|Server::getInstance\(\))(->isLevelLoaded\()/i' => '$1->getWorldManager()->isWorldLoaded(',
'/(->getServer\(\)|Server::getInstance\(\))(->loadLevel\()/i' => '$1->getWorldManager()->loadWorld(',
'/(->getServer\(\)|Server::getInstance\(\))(->unloadLevel\()/i' => '$1->getWorldManager()->unloadWorld(',
'/(->getServer\(\)|Server::getInstance\(\))(->setDefaultLevel\()/i' => '$1->getWorldManager()->setDefaultWorld(',
'/(->getLevelNonNull\(\))/i' => '->getWorld()',
'/(->getLevel\(\))/i' => '->getWorld()',
'/(->sendDataPacket\()/i' => '->getNetworkSession()$1',
'/(->dataPacket\()/i' => '->getNetworkSession()->sendDataPacket(',
'/(->getFood\()/i' => '->getHungerManager()$1',
'/(->getMaxFood\()/i' => '->getHungerManager()$1',
'/(->removeAllEffects\()/i' => '->getEffects()->clear(',
'/(->getWorld()->getName\()/i' => '->getWorld()->getFolderName()',
'/(->addEffect\()/i' => '->getEffects()->add(',
'/(->addTitle\()/i' => '->sendTitle(',
'/(->addSubTitle\()/i' => '->sendSubTitle(',
'/(->setFood\()/i' => '->getHungerManager()$1',
'/(->removeEffect\()/i' => '->getEffects()->remove(',
'/(->getEffect\()/i' => '->getEffects()->get(',
'/(->hasEffect\()/i' => '->getEffects()->has(',
'/(->getEffects\()/i' => '->getEffects()->all(',
'/(->isHungry\()/i' => '->getHungerManager()$1',
'/(->getSaturation\()/i' => '->getHungerManager()$1',
'/(->asVector3\()/i' => '->getPosition()$1',
'/(->setSaturation\()/i' => '->getHungerManager()$1',
'/(->addSaturation\()/i' => '->getHungerManager()$1',
'/(->getExhaustion\()/i' => '->getHungerManager()$1',
'/(->setExhaustion\()/i' => '->getHungerManager()$1',
'/(->exhaust\()/i' => '->getHungerManager()$1',
'/(->getXpLevel\()/i' => '->getXpManager()$1',
'/(->setXpLevel\()/i' => '->getXpManager()$1',
'/(->addXpLevels\()/i' => '->getXpManager()$1',
'/(->subtractXpLevels\()/i' => '->getXpManager()$1',
'/(->getXpProgress\()/i' => '->getXpManager()$1',
'/(->setXpProgress\()/i' => '->getXpManager()$1',
'/(->getCurrentTotalXp\()/i' => '->getXpManager()$1',
'/(->setCurrentTotalXp\()/i' => '->getXpManager()$1',
'/(->getLifetimeTotalXp\()/i' => '->getXpManager()$1',
'/(->setLifetimeTotalXp\()/i' => '->getXpManager()$1',
'/(->addXp\()/i' => '->getXpManager()$1',
'/(->subtractXp\()/i' => '->getXpManager()$1',
'/(->canPickupXp\()/i' => '->getXpManager()$1',
'/(->resetXpCooldown\()/i' => '->getXpManager()$1',
'/(->getDataPropertyManager\()/i' => '->getNetworkProperties(',
'/(Effect|\\\pocketmine\\\entity\\\Effect)(::getEffect\()/i' => '\\\pocketmine\\\data\\\bedrock\\\EffectIdMap::getInstance()->fromId(',
'/\$effect->getId\(\)/i' => '\\\pocketmine\\\data\\\bedrock\\\EffectIdMap::getInstance()->toId($effect)',
'/(Effect|\\\pocketmine\\\entity\\\Effect)(::registerEffect\()/i' => '\\\pocketmine\\\data\\\bedrock\\\EffectIdMap::getInstance()->register(',
'/(Effect|\\\pocketmine\\\entity\\\Effect)(::getEffectByName\()/i' => '\\\pocketmine\\\entity\\\effect\\\VanillaEffects::fromString(',
'/(Block|\\\pocketmine\\\block\\\Block)(::get\()/i' => '\\\pocketmine\\\block\\\BlockFactory::getInstance()->get(',
'/(Biome|\\\pocketmine\\\level\\\biome\\\Biome)(::getBiome\()/i' => '\\\pocketmine\\\world\\\biome\\\BiomeRegistry::getInstance()->getBiome(',
'/(Item|\\\pocketmine\\\item\\\Item)(::get\()/i' => '\\\pocketmine\\\item\\\ItemFactory::getInstance()->get(',
'/\$(p|player|target|sender)(->get(?:Floor)?[XYZ]\()/i' => '\$$1->getPosition()$2',
'/\$(p|player|target|sender)(->getYaw\()/i' => '\$$1->getLocation()$2',
'/\$(p|player|target|sender)(->getPitch\()/i' => '\$$1->getLocation()$2',
'/(Vector3::SIDE_)(NORTH|SOUTH|EAST|WEST)/i' => '\pocketmine\math\Facing::$2',
'/->getWorldHeight\(\)/i' => '->getMaxY()',
'/BlockIds/' => 'BlockLegacyIds',
'/(public|protected|private)\s(.*)(Level)(.*)/' => '$1 $2World$4', // converts Level object type in properties to World
'/(function|fn.*\(.*)Level([^,]*\$.*\))/' => '$1World$2', // converts Level parameter type in functions to World
'/(function .*\)\s*:\s*)Level(.*)/' => '$1World$2', // converts Level return type to World
'/public function onRun\(int \$currentTick\)(\s*:[a-zA-Z0-9_\s]*)(.*)/i' => 'public function onRun(): void $2', // task behaviour changed
'/->setHandler\(\);/' => '->setHandler(null);', // handler argument doesn't have a default - null should work for every other thing that is not around this context
'/implements\sPluginIdentifiableCommand/' => 'implements PluginOwned',
'/(Block|\\\pocketmine\\\block\\\Block)::([A-Z]*)/' => '\pocketmine\block\BlockLegacyIds::$2',
'/(Item|\\\pocketmine\\\item\\\Item)::([A-Z]*)/' => '\pocketmine\item\ItemIds::$2',
'/BlockFactory::getInstance\(\)->get\(([^,()]*)\)/' => 'BlockFactory::getInstance()->get($1, 0)',
'/Level(Load|Unload)Event/' => 'World$1Event',
'/\$(e|ev|event)->setCancelled\((?:true)?\)/' => '\$$1->cancel()',
'/\$(e|ev|event)->setCancelled\(false\)/' => '\$$1->uncancel()',
'/BaseLang/' => 'Language', // can we really convert this that easy?
'/([\({,\s\.])Generator::(.*)/' => '$1\pocketmine\world\generator\GeneratorManager::getInstance()->$2',
'/([\({,\s\.])GeneratorManager::(.*)/' => '$1\pocketmine\world\generator\GeneratorManager::getInstance()->$2',
'/DestroyBlockParticle/' => 'BlockBreakParticle',
'/(Entity|\\\pocketmine\\\entity\\\Entity)::registerEntity\((([^,]+)::class),*([^,]*),*(.*)\)/i' => '\\pocketmine\\entity\\EntityFactory::getInstance()->register($2, fn($world, $nbt) => new $3(\\pocketmine\\entity\\EntityDataHelper::parseLocation($nbt, $world), $nbt))'
];
const DANGEROUS_CODES = [
'/getYaw\(/' => 'Position based functions have been removed from player, entity and block and can be accessed via getPosition() / getLocation()',
'/getPitch\(/' => 'Position based functions have been removed from player, entity and block and can be accessed via getPosition() / getLocation()',
'/getGamemode\(/' => 'Player GameMode was made a class instead of an int',
'/getWorldManager\(\)->generateWorld\(/' => 'Parameters of World generation changed to WorldGenerateOptions',
'/add\(([^,\)]*(?:,)?){1,2}\)/i' => 'Additions to vectors require 3 arguments', // they are not actively changed since
'/subtract\(([^,\)]*(?:,)?){1,2}\)/i' => 'Subtractions of vectors require 3 arguments',
'/PlayerInteractEvent::(RIGHT|LEFT)_CLICK_AIR/' => 'Air clicks have been removed from interaction types.',
'/RemoteConsoleCommandSender/' => 'RemoteConsoleCommandSender was removed.',
'/EntityArmorChangeEvent/' => 'EntityArmorChangeEvent was removed.',
'/InventoryPickupArrowEvent/' => 'InventoryPickupArrowEvent was removed, use EntityItemPickupEvent instead.',
'/InventoryPickupItemEvent/' => 'InventoryPickupItemEvent was removed,use EntityItemPickupEvent instead.',
'/PlayerCheatEvent/' => 'PlayerIllegalMoveEvent was removed.',
'/PlayerIllegalMoveEvent/' => 'PlayerIllegalMoveEvent was removed.',
'/EntityLevelChangeEvent/' => 'EntityLevelChangeEvent was removed, use EntityTeleportEvent instead.',
'/CustomInventory/' => 'CustomInventory was removed in PM4',
'/InventoryEventProcessor/' => 'Class InventoryEventProcessor does no longer exist.',
'/(->getServer\(\)|Server::getInstance\(\))(->reload\()/i' => 'Method \pocketmine\Server::reload() was removed.',
'/(->getServer\(\)|Server::getInstance\(\))(->addPlayer\()/i' => 'Method \pocketmine\Server::addPlayer() was removed.',
'/ItemFactory::fromString\(/' => 'ItemFactory::fromString() was removed.',
'/Potion::getPotionEffectsById\(/' => 'Potion::getPotionEffectsById() was removed.',
'/CreativeInventoryAction/' => 'CreativeInventoryAction was removed.',
'/\$(e|ev|event)->setCancelled\(/' => 'Events are now cancelled with cancel() / uncancel() - Could not be replaced automatically'
];
$path = "./plugins/";
$folders = scandir($path, 1);
foreach($folders as $k){
if(is_dir($path.$k) && $k != ".. ") {
#echo($k);
$obj = scandir($path.$k);
$pluginFolder = load_plugin_folder($path.$k);
$outputFolder = __DIR__ . DIRECTORY_SEPARATOR . 'output' . DIRECTORY_SEPARATOR . basename($pluginFolder) . DIRECTORY_SEPARATOR;
log_notice('Загрузка плагина: ' . $k);
copy_folder_structure($pluginFolder, $outputFolder);
repair_files($pluginFolder, $outputFolder);
log_notice('Начинаем конвертацию..');
convert_plugin_file($pluginFolder . 'plugin.yml', $outputFolder . 'plugin.yml', $mainPath);
}
}
/*
$pluginFolder = load_plugin_folder($obj);
$outputFolder = __DIR__ . DIRECTORY_SEPARATOR . 'output' . DIRECTORY_SEPARATOR . basename($pluginFolder) . DIRECTORY_SEPARATOR;
log_notice('Загрузка плагина: ' . $obj);
copy_folder_structure($pluginFolder, $outputFolder);
repair_files($pluginFolder, $outputFolder);
log_notice('Чиним плагин');
convert_plugin_file($pluginFolder . 'plugin.yml', $outputFolder . 'plugin.yml', $mainPath);
log_notice('Конвертация в API4 завершено за: ' . round((microtime(true) * 1000) - $t, 2) . 'мс.');
*/
function repair_files(string $pluginFolder, string $outputFolder) {
$fileCount = count_files($pluginFolder);
#echo 'Repairing plugin files' . str_repeat(' ', strlen($fileCount)) . '(0/' . $fileCount . ')';
$baseLen = 3 + strlen($fileCount);
$count = 0;
$warnings = [];
scan_directory_recursively($pluginFolder, function (string $path) use ($pluginFolder, $outputFolder, $fileCount, $baseLen, &$count, &$warnings): void {
if (is_dir($path)) return;
$targetPath = $outputFolder . ($relative = substr($path, strlen($pluginFolder)));
if (!str_ends_with($path, '.php')) {
file_put_contents($targetPath, file_get_contents($path));
} else {
repair_php_file($path, $targetPath, $relative, $warnings);
}
++$count;
log_notice("Конвертация файлов (" . $count . '/' . $fileCount . ')');
});
#echo PHP_EOL;
if (count($warnings) == 0) { log_notice('В данном плагине не было найдено строк которые нельзя было бы переконвертировать плагин на API 4.0 '); foreach ($warnings as $w) log_warning(' - ' . $w); }
else { log_warning('Было найдено: ' . count($warnings) . ' ошибок. Они могут конфликтовать с текущим API 4.0, из-за чего плагин может не запуститься'); }
}
function repair_php_file(string $path, string $targetPath, string $relativePath, array &$warnings) {
$content = file_get_contents($path);
foreach (IMPORT_REMAPS as $old => $new) {
$content = str_replace('\\' . $old, $new, $content);
$content = str_replace($old, $new, $content);
}
foreach (REMAPS as $regex => $v) {
$content = preg_replace($regex, $v, $content);
}
foreach (preg_split("/\r\n|\n|\r/", $content) as $k => $line) {
foreach (DANGEROUS_CODES as $m => $msg) {
if (preg_match($m, $line)) $warnings[] = $relativePath . ':' . ($k + 1) . ' ' . $msg;
}
}
file_put_contents($targetPath, $content);
}
function count_files(string $directory): int {
$count = 0;
scan_directory_recursively($directory, function (string $path) use (&$count) {
if (!is_dir($path)) $count++;
});
return $count;
}
function scan_directory_recursively(string $path, Closure $closure) {
$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
foreach (array_diff(scandir($path), ['.', '..']) as $file) {
$closure($path . $file);
if (is_dir($path . $file)) scan_directory_recursively($path . $file, $closure);
}
}
function convert_plugin_file(string $path, string $outputPath, &$mainPath) {
$yaml = yaml_parse_file($path);
$yaml['api'] = '4.0.0';
$p = [];
$recursion = function (array $permissions, Closure $handler) use (&$p) {
foreach ($permissions as $str => $permissionData) {
if (isset($permissionData['children'])) {
$handler($permissionData['children'], $handler);
unset($permissionData['children']);
}
$p[$str] = $permissionData;
}
};
$recursion($yaml['permissions'] ?? [], $recursion);
if (isset($yaml['permissions'])) $yaml['permissions'] = $p;
yaml_emit_file($outputPath, $yaml);
$mainPath = dirname($path) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . $yaml['main'] . '.php';
}
function copy_folder_structure(string $src, string $target, bool $__do_not_change = true, &$failureCount = 0) {
foreach (array_diff(scandir($src), ['.', '..']) as $file) {
if (is_dir($src . $file)) {
if (!@mkdir($target . $file, 0777, true)) ++$failureCount;
copy_folder_structure($src . $file . DIRECTORY_SEPARATOR, $target . $file . DIRECTORY_SEPARATOR, false, $failureCount);
}
}
#if ($__do_not_change and $failureCount) log_warning($failureCount . ' directories were unable to be generated, maybe already existent?');
}
function load_plugin_folder(string $dir): string {
$dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (!file_exists($dir . 'plugin.yml')) {log_notice('Конвертация в API4 завершена!');exit;}
if (!is_dir($dir . 'src')) {log_notice('Конвертация в API4 завершена!');exit;}
return $dir;
}
function log_notice(string $str) {
echo "\033[92m [ Convert/INFO ] :: " . $str . "\033[39m" . PHP_EOL;
}
function log_warning(string $str) {
echo "\033[93m [ Convert/WARNING ] :: " . $str . "\033[39m" . PHP_EOL;
}
function log_error(string $str) {
echo "\033[91m [ Convert/ERROR ] :: " . $str . "\033[39m" . PHP_EOL;
}
Читай код и понимай как работает
Да, я нихуя нипон куда его пихать