I’ve always wanted to make a Minecraft mod. Sadly, I was never very fond of re-learning Java, and that always seemed to be a requirement. Until recently.

Thanks to dogged persistence, I’ve actually discovered a way to make Minecraft mods, without really knowing Java. There are a few tricks and caveats that will let us make all the mods we desire, from the comfort of our own PHP.

This is just half of the adventure. In another post we’ll see a neat 3D JavaScript Minecraft editor. If that sounds like something you’d like to learn, be sure to check that post out.

Most of the code for this tutorial can be found on Github. I’ve tested all of the JavaScript bits in the latest version of Chrome and all the PHP bits in PHP 7.0. I can’t promise it will look exactly the same in other browsers, or work the same in other versions of PHP, but the core concepts are universal.

Setting Things Up

As you’ll see in a bit, we’re going to be communicating loads between PHP and a Minecraft server. We’ll need a script to run for as long as we need the mod’s functionality. We could use a traditional busy loop:

while ( true ) { sleep ( 1 ) ; }

…Or we could do something a little more interesting.

I’ve grown quite fond of AMPHP. It’s a collection of asynchronous PHP libraries, including things like HTTP servers and clients, and an event loop. Don’t worry if you’re unfamiliar with these things. We’ll take it nice and slow.

Let’s begin by creating an event loop, and a function to watch for changes to a file. We need to install the event loop and filesystem libraries:

composer require amphp/amp composer require amphp/file

Then, we can start up an event loop, and check to make sure it’s running as expected:

require __DIR__ . "/vendor/autoload.php" ; Amp\ run ( function ( ) { Amp\ repeat ( function ( ) { } , 1000 ) ; } ) ;

This is similar to the infinite loop we had, except that it’s non-blocking. This means we’ll be able to perform more concurrent operations, while waiting for operations that would normally block the process.

A Short Detour through the Land of Promises

In addition to this wrapper code, AMPHP also provides a neat promise-based interface. You may already be familiar with this concept (from JavaScript), but here’s a quick example:

$eventually = asyncOperation ( ) ; $eventually - > then ( function ( $data ) { } ) - > catch ( function ( Exception $e ) { } ) ;

Promises are a way to represent data that we don’t yet have — eventual values. It may be something slow (like a filesystem operation or an HTTP request).

The point is that we don’t immediately have the value. And instead of waiting for the value in the foreground (which would traditionally block the process), we wait for it in the background. While waiting in the background, we can do other meaningful work in the foreground.

AMPHP takes promises a step further, using generators. This is all a bit intense to explain in a single sitting, but bear with me.

Generators are a syntactic simplification of iterators. That is, they reduce the amount of code we need to write, to enable iterating over values not yet defined in an array. Additionally, they make it possible to send data into the function that generates these values (while it’s generating). Starting to sense a pattern here?

Generators allow us to build the next array item on demand. Promises represent an eventual value. Therefore, we can repurpose generators to generate a list of steps (or behavior), which are executed on demand.

This may be easier to understand by looking at some code:

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 ; }

Let’s think about how this would work in synchronous execution:

Call to getContents Call to $files->mtime($path) (imagine this was just a proxy to filemtime ) Wait for filemtime to return Call to $files->get($path) (imagine this was just a proxy to file_get_contents ) Wait for file_get_contents to return

With promises, we can avoid blocking, at the cost of a few new closures:

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

Since promises are chain-able, we could reduce this to:

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

I don’t know about you, but this still seems kinda messy to me. So how do generators fit into this? Well, AMPHP uses the yield keyword to evaluate promises. Let’s look at the getContents function again:

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

$files->mtime($path) returns a promise. Instead of waiting for the lookup to complete, the function stops running as it encounters the yield keyword. After a while, AMPHP is notified that the stat operation is complete, and it resumes this function.

Then, if the timestamps don’t match, files->get($path) fetches the contents. This is another blocking operation, so yield suspends the function again. When the file is read, AMPHP will start this function up again (returning the file contents).

This code looks similar to the synchronous alternative, but is using promises (transparently) and generators to make it non-blocking.

AMPHP differs a little from the Promises A+ spec in that the AMPHP promises don’t support a then method. Other PHP implementations, like React/Promise and Guzzle Promises do. The important thing is understanding the eventual nature of promises, and how they can be interfaced with generators, to support this succinct async syntax.

Listening to Logs

Last time I wrote about Minecraft, it was about using the door of a Minecraft house to trigger a real-world alarm. In that, we briefly covered to process of getting data out of a Minecraft server, and into PHP.

We’ve taken a bit longer to get there, this time round, but we’re essentially doing the same thing. Let’s look at the code to identify player commands:

define ( "LOG_PATH" , "/path/to/logs/latest.log" ) ; $files = Amp\ File \ filesystem ( ) ; $commands = [ ] ; $timestamp = yield $filesystem - > mtime ( LOG_PATH ) ; 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 ) { array_push ( $commands , $line ) ; print "executing: " . $line . PHP_EOL ; break ; } } } } , 500 ) ;

We start off by getting the reference file timestamp. We use this to work out if the file has changed (in the getContents function). We also create an empty list, where we’ll store all the commands we’ve already executed. This list will help us avoid executing the same command twice.

You need to replace /path/to/logs/latest.log with the path to your Minecraft server’s log files. I recommend running the stand-alone Minecraft server, which should put logs/latest.log in the root directory.

We’ve told Amp\repeat to run this closure every 500 milliseconds. In that time, we check for file changes. If the timestamp has changed, we split the log file’s lines into an array and reverse it (so that we’re reading the most recent messages first).

If a line contains “> >” (as would happen if a player typed “> some command”), we assume that line contains a command instruction.

Creating Blueprints

One of the most time-consuming things in Minecraft is building large structures. It would be much easier if I could plan them out (using some swanky 3D JavaScript builder), and then place them in the world using a special command.

We can use a slightly modified version, of the builder I covered in the other aforementioned post to generate a list of custom block placements:

At the moment, this builder only allows the placement of dirt blocks. The array structure it generates is the x , y , and z coordinates of each dirt block placed (after the initial scene is rendered). We can copy this into the PHP script we’ve been working on. We should also figure out how to identify the exact command to build whatever structure we design:

$isCommand = stristr ( $line , "> >" ) !== false ; $isNotRepeat = ! in_array ( $line , $commands ) ; if ( $isCommand && $isNotRepeat ) { array_push ( $commands , $line ) ; executeCommand ( $line ) ; break ; } function executeCommand ( $raw ) { $command = trim ( substr ( $raw , stripos ( $raw , "> >" ) + 3 ) ) ; if ( $command === "build" ) { $blocks = [ ] ; foreach ( $block as $block ) { } } }

Each time we receive a command, we can pass it to the executeCommand function. There we extract from the second > to the end of the line. We only need to identify build commands at the moment.

Talking to the Server

Listening to logs is one thing, but how do we communicate back to the server? The stand-alone server launches an admin chat server (called RCON). This is the same admin chat server that enables mods in other games, like Counter-Strike.

Turns out someone has already built an RCON client (albeit blocking), and recently I wrote a nice wrapper for this. We can install it with:

composer require theory/builder

Let me apologize for how big that library is. I included a version of the Minecraft stand-alone server, so that I could build automated tests for the library. What a rush…

We need to configure our stand-alone server so that we can make RCON connections to it. Add the following to the server.properties file, in the same folder as the server jar :

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

After a restart, we should be able to connect to the server using code resembling the following:

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

We can retrofit our executeCommand function to build a complete structure:

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 = [ ] ; $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 ) ; } } }

The new and improved executeCommand function checks to see if the command (a message resembling <player_name> > build ) starts with the word “build”.

If the builder was non-blocking, it would be much better to use yield new Amp\Pause(500) , instead of usleep(500000) . We’d also need to treat executeCommand as a generator function, where we call it, which means using yield executeCommand(...) .

If it does, the command is split by spaces, to get the x , y , and z coordinates where the design should be built. Then it takes the array we generated from the designer, and places each block in the world.

Where To From Here?

You can probably imagine many fun extensions of this simple mod-like script we just created. The designer could be expanded to create arrangements consisting of many different kinds and configurations of blocks.

The mod script could be extended to receive updates through a JSON API, so that the designer could submit named designs, and the build command could specify exactly which design the player wants built.

I’ll leave those ideas as an exercise for you. Don’t forget to check out the companion JavaScript post, and if you have any ideas or comments to share, please do so in the comments!