Статьи

Моддинг Minecraft с PHP — Здания из кода!

Я всегда хотел сделать мод Minecraft. К сожалению, я никогда не любил переучивать Java, и это всегда казалось требованием. До не давнего времени.

Заставка Minecraft

Благодаря упорным постоянством, я на самом деле нашел способ сделать Minecraft модов, не зная Java. Есть несколько уловок и предостережений, которые позволят нам сделать все моды, которые мы хотим, не выходя из нашего собственного PHP.

Это только половина приключения. В другом посте мы увидим аккуратный 3D- редактор JavaScript Minecraft . Если это звучит как то, что вы хотели бы узнать, не забудьте проверить этот пост.

Большая часть кода для этого урока может быть найдена на Github . Я протестировал все биты JavaScript в последней версии Chrome и все биты PHP в PHP 7.0. Я не могу обещать, что в других браузерах он будет выглядеть точно так же или работать в других версиях PHP, но основные концепции универсальны.

Настройка вещей

Как вы увидите позже, мы собираемся обмениваться данными между PHP и сервером Minecraft. Нам понадобится скрипт для запуска до тех пор, пока нам нужна функциональность мода. Мы могли бы использовать традиционный занятый цикл:

while (true) {
    // listen for player requests
    // make changes to the game

    sleep(1);
}

… Или мы могли бы сделать что-нибудь более интересное.

Я полюбил AMPHP . Это коллекция асинхронных библиотек PHP, включая такие вещи, как HTTP-серверы и клиенты, а также цикл обработки событий. Не волнуйтесь, если вы не знакомы с этими вещами. Мы возьмем это красиво и медленно.

Давайте начнем с создания цикла событий и функции для отслеживания изменений в файле. Нам нужно установить цикл обработки событий и библиотеки файловой системы:

 composer require amphp/amp
composer require amphp/file

Затем мы можем запустить цикл обработки событий и убедиться, что он работает должным образом:

 require __DIR__ . "/vendor/autoload.php";

Amp\run(function() {
    Amp\repeat(function() {
        // listen for player requests
        // make changes to the game
    }, 1000);
});

Это похоже на бесконечный цикл, который у нас был, за исключением того, что он не блокирует. Это означает, что мы сможем выполнять больше параллельных операций, ожидая операций, которые обычно блокируют процесс.

Короткий обход через страну обещаний

В дополнение к этому коду обертки AMPHP также предоставляет удобный интерфейс на основе обещаний . Возможно, вы уже знакомы с этой концепцией (из JavaScript), но вот небольшой пример:

 $eventually = asyncOperation();

$eventually
    ->then(function($data) {
        // do something with $data
    })
    ->catch(function(Exception $e) {
        // oops, something went wrong!
    });
в use Amp\File\Driver;

function getContents(Driver $files, $path, $previous) {
    $next = yield $files->mtime($path);

    if ($previous !== $next) {
        return yield $files->get($path);
    }

    return null;
}

Обещания — это способ представления данных, которых у нас еще нет — возможные значения. Это может быть что-то медленное (например, операция файловой системы или запрос HTTP).

Дело в том, что мы не сразу имеем значение. И вместо ожидания значения на переднем плане (которое традиционно блокирует процесс), мы ожидаем его на заднем плане. В ожидании на заднем плане мы можем выполнять другую значимую работу на переднем плане.

AMPHP делает обещания на шаг вперед, используя генераторы . Это все немного сложно объяснить за один присест, но терпите меня.

Генераторы являются синтаксическим упрощением итераторов. То есть они уменьшают объем кода, который нам нужно написать, чтобы обеспечить возможность перебора значений, еще не определенных в массиве. Кроме того, они позволяют отправлять данные в функцию, которая генерирует эти значения (пока она генерирует). Начинаете ощущать здесь паттерн?

Генераторы позволяют нам создавать следующий элемент массива по требованию. Обещания представляют возможную ценность. Следовательно, мы можем переназначить генераторы для генерации списка шагов (или поведения), которые выполняются по требованию.

Это может быть легче понять, посмотрев на некоторый код:

 getContents

Давайте подумаем, как это будет работать в синхронном исполнении:

  1. Позвоните, чтобы получить содержание
  2. Вызов $files->mtime($path)filemtime )
  3. Подождите, пока filemtime
  4. Вызовите $files->get($path)file_get_contents )
  5. Подождите, пока file_get_contents

С обещаниями мы можем избежать блокировок за счет нескольких новых закрытий:

 function getContents($files, $path, $previous) {
    $files->mtime($path)->then(
        function($next) use ($previous) {
            if ($previous !== $next) {
                $files->get($path)->then(
                    function($data) {
                        // do something with $data
                    }
                )
            }

            // do something with null
        }
    );
}

Поскольку обещания являются цепочечными, мы можем сократить это до:

 function getContents($files, $path, $previous) {
    $files->mtime($path)->then(
        function($next) use ($previous) {
            if ($previous !== $next) {
                return $files->get($path);
            }

            // do something with null
        }
    )->then(
        function($data) {
            // do something with data
        }
    );
}

Я не знаю о вас, но это все еще кажется мне немного грязным. Так как генераторы вписываются в это? Ну, AMPHP использует ключевое слово yield Давайте снова посмотрим на функцию getContents

 function getContents(Driver $files, $path, $previous) {
    $next = yield $files->mtime($path);

    if ($previous !== $next) {
        return yield $files->get($path);
    }

    return null;
}

$files->mtime($path) Вместо того, чтобы ждать завершения поиска, функция останавливается, поскольку встречает ключевое слово yield Через некоторое время AMPHP получает уведомление о завершении операции stat и возобновляет эту функцию.

Затем, если временные метки не совпадают, files->get($path) Это еще одна операция блокировки, так что yield Когда файл прочитан, AMPHP снова запустит эту функцию (возврат содержимого файла).

Этот код похож на синхронную альтернативу, но использует обещания (прозрачно) и генераторы, чтобы сделать его неблокирующим.

AMPHP немного отличается от спецификации Promises A + тем, что обещания AMPHP не поддерживают метод then Другие реализации PHP, такие как React / Promise и Guzzle Promises . Важно понимать конечную природу обещаний и то, как их можно связать с генераторами, чтобы поддержать этот краткий асинхронный синтаксис.

Прослушивание логов

В прошлый раз, когда я писал о Minecraft , речь шла об использовании двери дома Minecraft, чтобы вызвать реальную тревогу. В этом мы кратко рассмотрели процесс получения данных с сервера Minecraft и в PHP.

На этот раз мы потратили немного больше времени, но по сути дела делаем то же самое. Давайте посмотрим на код для определения команд игрока:

 define("LOG_PATH", "/path/to/logs/latest.log");

$files = Amp\File\filesystem();

// get reference data

$commands = [];
$timestamp = yield $filesystem->mtime(LOG_PATH);

// listen for player requests

Amp\repeat(function() use ($files, &$commands, &$timestamp) {
    $contents = yield from getContents(
        $files, LOG_PATH, $timestamp
    );

    if (!empty($contents)) {
        $lines = array_reverse(explode(PHP_EOL, $contents));

        foreach ($lines as $line) {
            $isCommand = stristr($line, "> >") !== false;
            $isNotRepeat = !in_array($line, $commands);

            if ($isCommand && $isNotRepeat) {
                // execute mod command

                array_push($commands, $line);

                print "executing: " . $line . PHP_EOL;
                break;
            }
        }
    }
}, 500);

Мы начнем с получения контрольной отметки временного файла. Мы используем это, чтобы выяснить, изменился ли файл (в функции getContents Мы также создаем пустой список, в котором мы будем хранить все команды, которые мы уже выполнили. Этот список поможет нам избежать выполнения одной и той же команды дважды.

Вам нужно заменить /path/to/logs/latest.log Я рекомендую запустить автономный сервер Minecraft , который должен поместить logs/latest.log

Мы сказали Amp\repeat500 В это время мы проверяем изменения файла. Если временная метка изменилась, мы разбиваем строки файла журнала на массив и переворачиваем его (так что сначала мы читаем самые последние сообщения).

Если строка содержит «>>» (как это произошло бы, если игрок набрал «> некоторую команду»), мы предполагаем, что строка содержит инструкцию команды.

распознавание команд

Создание чертежей

Одна из самых трудоемких вещей в Minecraft — это строительство больших конструкций. Было бы намного проще, если бы я мог их спланировать (используя какой-то шикарный 3D-конструктор JavaScript), а затем поместить их в мир, используя специальную команду.

Мы можем использовать слегка модифицированную версию компоновщика, который я описал в другом вышеупомянутом посте, чтобы сгенерировать список пользовательских блоков:

Создание пользовательских блоков размещения

На данный момент этот застройщик допускает размещение только грязевых блоков. Генерируемая им структура массива — это координаты xyz Мы можем скопировать это в скрипт PHP, над которым мы работаем. Мы также должны выяснить, как определить точную команду для построения любой структуры, которую мы проектируем:

 $isCommand = stristr($line, "> >") !== false;
$isNotRepeat = !in_array($line, $commands);

if ($isCommand && $isNotRepeat) {
    array_push($commands, $line);
    executeCommand($line);
    break;
}

// ...later

function executeCommand($raw) {
    $command = trim(
        substr($raw, stripos($raw, "> >") + 3)
    );

    if ($command === "build") {
        $blocks = [
            // ...from the 3D builder
        ];

        foreach ($block as $block) {
            // ... place each block
        }
    }
}

Каждый раз, когда мы получаем команду, мы можем передать ее в функцию executeCommand Там мы извлекаем от второго > Нам нужно только определить команды build

Разговор с сервером

Прослушивание логов — это одно, но как мы можем общаться с сервером? Автономный сервер запускает сервер чата администратора (называется RCON). Это тот же сервер администратора, который включает моды в других играх, таких как Counter-Strike.

Оказывается, кто-то уже создал клиент RCON (хотя и блокирует), и недавно я написал для этого хорошую оболочку. Мы можем установить его с:

 composer require theory/builder

Позвольте мне извиниться за то, насколько велика эта библиотека. Я включил версию автономного сервера Minecraft, чтобы я мог создавать автоматизированные тесты для библиотеки. Что за спешка…

Нам нужно настроить наш автономный сервер, чтобы мы могли подключаться к нему через RCON. Добавьте следующее в файл server.propertiesjar

 enable-query=true
enable-rcon=true
query.port=25565
rcon.port=25575
rcon.password=password

После перезапуска мы сможем подключиться к серверу, используя код, похожий на следующий:

 $builder = new Client("127.0.0.1", 25575, "password");
$builder->exec("/say hello world");

Мы можем модифицировать нашу функцию executeCommand

 function executeCommand($builder, $raw) {
    $command = trim(
        substr($raw, stripos($raw, "> >") + 3)
    );

    if (stripos($command, "build") === 0) {
        $parts = explode(" ", $command);

        if (count($parts) < 4) {
            print "invalid coordinates";
            return;
        }

        $x = $parts[1];
        $y = $parts[2];
        $z = $parts[3];

        $blocks = [
            // ...from the 3D builder
        ];

        $builder->exec("/say building...");

        foreach ($blocks as $block) {
            $dx = $block[0] + $x;
            $dy = $block[1] + $y;
            $dz = $block[2] + $z;

            $builder->exec(
                "/setblock {$dx} {$dy} {$dz} dirt"
            );

            usleep(500000);
        }
    }
}

Новая и улучшенная функция executeCommand<player_name> > build

Если бы конструктор был неблокирующим, было бы намного лучше использовать yield new Amp\Pause(500)usleep(500000) Нам также нужно обрабатывать executeCommandyield executeCommand(...)

Если это так, команда разделяется пробелами, чтобы получить координаты xyz Затем он берет массив, сгенерированный нами из конструктора, и размещает каждый блок в мире.

«готовый» продукт

Где отсюда?

Вероятно, вы можете представить множество забавных расширений этого простого мода-подобного скрипта, который мы только что создали. Конструктор может быть расширен для создания аранжировок, состоящих из множества различных видов и конфигураций блоков.

Сценарий мода может быть расширен для получения обновлений через JSON API, чтобы разработчик мог отправлять именованные проекты, а команда build

Я оставлю эти идеи в качестве упражнения для вас. Не забудьте проверить сопутствующий пост JavaScript , и если у вас есть какие-либо идеи или комментарии, пожалуйста, делайте это в комментариях!