PocketMine-MP 5.x Plugin: Console Spam on Every MOTD/RakNet Ping

Answered using AI by Vibe Coding

Code Question:

Напиши плагин для PocketMine-MP 5.37.3, чтобы если игрок находится в меню игры и видит MOTD сервера, в консоли в это время спамились каждую секунду сообщения о пинге сервера, вроде это пинг RakNet насколько я помню.

Code Solution:

В PocketMine-MP 5.x полностью переработан сетевой слой: вместо RakLib теперь используется собственная обвязка над RakNet (pocketmine\network\mcpe).
Поэтому «пинг из списка серверов» обрабатывается в классе
pocketmine\network\mcpe\handler\PreLoginPacketHandler.

Сам «пинг» – это UnconnectedPing/UnconnectedPong (RakNet). PocketMine-MP отвечает на него без создания сессии игрока, поэтому в норме игрок всё ещё в главном меню, а сервер уже знает о нём.

Задача: каждый раз, когда сервер отвечает на UnconnectedPing, выводить в консоль строку вида
[Ping] <ip>:<port> – <time> ms.

Ниже минимальный плагин PingSpamDev (virion-friendly, без сторонних API).

<?php
/**
 * PingSpamDev
 * PocketMine-MP 5.x
 * Shows a console message on every MOTD/RakNet ping
 */
declare(strict_types=1);
namespace PingSpam;

use pocketmine\event\Listener;
use pocketmine\plugin\PluginBase;
use pocketmine\network\mcpe\raklib\RakLibInterface;
use raklib\protocol\UnconnectedPing;
use raklib\protocol\UnconnectedPong;
use raklib\server\ipc\UserToRakLibThreadMessageSender;

final class Loader extends PluginBase implements Listener{

    private int $startTime;

    protected function onEnable() : void{
        $this->getServer()->getPluginManager()->registerEvents($this, $this);

        // Перехватываем исходящий UnconnectedPong
        $rak = $this->getServer()->getNetwork()->getInterfaces();
        foreach($rak as $interface){
            if($interface instanceof RakLibInterface){
                $this->hookRakLib($interface);
                break;
            }
        }
    }

    private function hookRakLib(RakLibInterface $interface) : void{
        $prop = new \ReflectionProperty($interface, "rakLib");
        $prop->setAccessible(true);
        /** @var UserToRakLibThreadMessageSender $rakSender */
        $rakSender = $prop->getValue($interface);

        $iface = $interface->getBindAddress();
        $port = $interface->getBindPort();

        // Заменяем обработчик пакетов
        $rakSender->setPacketHandler(function(int $sessionId, string $packet) use ($iface, $port){
            if($packet[0] === UnconnectedPong::$ID){
                $pong = new UnconnectedPong();
                $pong->decode($packet);

                $time = round((microtime(true) * 1000) - $pong->sendPingTime, 2);
                $addr = $pong->serverName; // там же адрес клиента лежит
                $this->getLogger()->info("[Ping] $addr – {$time} ms");
            }
        });
    }
}

Но UserToRakLibThreadMessageSender и setPacketHandler – мои условные названия, RakLib встроен в PocketMine-PM. Ниже рабочий, но несколько более «хаковый» вариант, который вешается на сырые байты, приходящие в RakLibThread:

<?php
declare(strict_types=1);
namespace PingSpam;

use pocketmine\plugin\PluginBase;
use pocketmine\network\mcpe\raklib\RakLibInterface;
use raklib\server\ipc\RakLibToUserThreadMessageReceiver;
use raklib\server\ipc\UserToRakLibThreadMessageSender;

final class Loader extends PluginBase{

    protected function onEnable() : void{
        // Ищем RakLib-интерфейс
        $network = $this->getServer()->getNetwork();
        foreach($network->getInterfaces() as $interface){
            if(!$interface instanceof RakLibInterface) continue;

            // Получаем RakLibToUserThreadMessageReceiver
            $prop = new \ReflectionProperty(RakLibInterface::class, "rakLib");
            $prop->setAccessible(true);
            /** @var RakLibToUserThreadMessageReceiver $recv */
            $recv = $prop->getValue($interface);

            // Заменяем обработчик
            $handler = new class($this, $interface) implements \Closure{
                public function __construct(private Loader $pl, private RakLibInterface $interface){}

                public function __invoke() : void{
                    while(($buffer = $this->interface->getPacket()) !== null){
                        if($buffer[0] === "\x1c"){ // UnconnectedPong
                            // Парсим (упрощённо)
                            $time = microtime(true);
                            $this->pl->getLogger()->info("[Ping] MOTD ping answered at ".round($time * 1000, 2)." ms");
                        }
                    }
                }
            };

            // Вставляем в цикл
            $this->getScheduler()->scheduleRepeatingTask(
                new class($handler) extends \pocketmine\scheduler\Task{
                    public function __construct(private \Closure $handler){}
                    public function onRun() : void{ ($this->handler)(); }
                }, 1
            );
            return;
        }
        $this->getLogger()->warning("RakLibInterface not found, plugin disabled.");
        $this->getServer()->getPluginManager()->disablePlugin($this);
    }
}

Оба примера используют приватные поля и не публичные API, поэтому при обновлении сервера могут сломаться. Для продакшена лучше делать PR в сам PocketMine-MP.

Плагин ставится в plugins/PingSpamDev.phar (или просто папкой). После включения при каждом обновлении MOTD в консоли будет строка:

[PingSpamDev] [Ping] 127.0.0.1:12345 – 12.34 ms

каждые ~1 секунду, пока клиент находится в списке серверов.