Я всегда хотел сделать мод Minecraft. К сожалению, я никогда не любил переучивать Java, и это всегда казалось требованием. До не давнего времени.
Благодаря упорным постоянством, я на самом деле нашел способ сделать 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
Давайте подумаем, как это будет работать в синхронном исполнении:
- Позвоните, чтобы получить содержание
- Вызов
$files->mtime($path)
filemtime
) - Подождите, пока
filemtime
- Вызовите
$files->get($path)
file_get_contents
) - Подождите, пока
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\repeat
500
В это время мы проверяем изменения файла. Если временная метка изменилась, мы разбиваем строки файла журнала на массив и переворачиваем его (так что сначала мы читаем самые последние сообщения).
Если строка содержит «>>» (как это произошло бы, если игрок набрал «> некоторую команду»), мы предполагаем, что строка содержит инструкцию команды.
Создание чертежей
Одна из самых трудоемких вещей в Minecraft — это строительство больших конструкций. Было бы намного проще, если бы я мог их спланировать (используя какой-то шикарный 3D-конструктор JavaScript), а затем поместить их в мир, используя специальную команду.
Мы можем использовать слегка модифицированную версию компоновщика, который я описал в другом вышеупомянутом посте, чтобы сгенерировать список пользовательских блоков:
На данный момент этот застройщик допускает размещение только грязевых блоков. Генерируемая им структура массива — это координаты x
y
z
Мы можем скопировать это в скрипт 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.properties
jar
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)
Нам также нужно обрабатывать executeCommand
yield executeCommand(...)
Если это так, команда разделяется пробелами, чтобы получить координаты x
y
z
Затем он берет массив, сгенерированный нами из конструктора, и размещает каждый блок в мире.
Где отсюда?
Вероятно, вы можете представить множество забавных расширений этого простого мода-подобного скрипта, который мы только что создали. Конструктор может быть расширен для создания аранжировок, состоящих из множества различных видов и конфигураций блоков.
Сценарий мода может быть расширен для получения обновлений через JSON API, чтобы разработчик мог отправлять именованные проекты, а команда build
Я оставлю эти идеи в качестве упражнения для вас. Не забудьте проверить сопутствующий пост JavaScript , и если у вас есть какие-либо идеи или комментарии, пожалуйста, делайте это в комментариях!