PHP
Article

Modding Minecraft with PHP – Buildings from Code!

By Christopher Pitt

Working with PHP 7.1? Download our FREE PHP 7.1 Cheat Sheet!

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.

Minecraft splash screen

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) {
    // listen for player requests
    // make changes to the game

    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() {
        // listen for player requests
        // make changes to the game
    }, 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) {
        // do something with $data
    })
    ->catch(function(Exception $e) {
        // oops, something went wrong!
    });

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:

  1. Call to getContents
  2. Call to $files->mtime($path) (imagine this was just a proxy to filemtime)
  3. Wait for filemtime to return
  4. Call to $files->get($path) (imagine this was just a proxy to file_get_contents)
  5. 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) {
                        // do something with $data
                    }
                )
            }

            // do something with null
        }
    );
}

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

            // do something with null
        }
    )->then(
        function($data) {
            // do something with 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();

// 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);

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.

recognizing commands

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:

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

// ...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
        }
    }
}

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 = [
            // ...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);
        }
    }
}

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.

the "finished" product

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!

  • Clark Winkelmann

    Very nice ! But do this RCON client allows to retrieve “events” ? I believe there would be no other choice than polling a command ? For example, can you do the same door alarm thing without command blocks ?

    I could not find any command listing online so I think only standard administrative commands are available ?Hope you do more Minecraft tutorials :D

    • Chris

      You do have to poll, unfortunately. There’s no event listener system, as far as I am aware. You can do anything, through RCON, that you can do with a command block. Well, most things. The differences are not worth mentioning. There are reasons why you’d rather want to do some things using command blocks, even if you dynamically create the command blocks, through RCON. But that’s a topic for another post… :)

      • Clark Winkelmann

        After the self-updating CRON tab, here come the self replicating command block :D

  • ceagle

    Nice to see someone do it in an open-source manner. I’ve been using a very similar concept since about June 2015 I think. I’m just not sharing it, because it’s heavily tied into our server network. Based on such a PHP-based plugin-server, most of our commands are nowadays being processed with PHP – and about half a year ago, I started developing a dungeon-game using instance areas in Minecraft, all based on the same base concept.

    The one stupid issue with reading logs though: whenever a real day ends and the server starts using a fresh logfile… some commands might be ignored silently. ;)

    If you’re using a PHP-based plugin-server on a huge scale, you might wanna split all of those “plugins” up into separate processes though. For example: my PHP plugin server is just a single-threaded process using the option you decided not to use: a while(true) loop. Each of the connected servers has a dynamic read-log interval – the more frequently anything gets in from a specific server, the quicker the interval gets, in order to reduce delays reactively.

    And whenever it executes something, it launches an additional PHP process, providing it with all info necessary via command parameters. RAM usage is so low that even 12 MB won’t even be noticed on my rootserver with 128 GB of RAM. ;-)

    While developing the before mentioned dungeon-game, in some cases I did end up with silly infinite-loops and the process would never stop unless killed. But only this one process would hang, while everything else keeps running flawlessly. How would you handle such a situation?

    Also, if you’d like to exchange some ideas or inspiration, I’d love to.

    • Chris

      “How would you handle such a situation?”

      I’ve only created a couple big projects, using this technique, so far. Both of them use an event-loop abstraction, and the plugin managers (which read logs and poll testfor/testforblock) are meant to run continuously (unless their process is terminated). Can’t say it’s been a problem for me, yet. I’m busy writing about a cooperative coding/survival game, which I’d love to be able to scale to multiple player groups per server. I guess I’ll need a more robust plugin system and area generator approach, to make that happen…

      “if you’d like to exchange some ideas or inspiration, I’d love to”

      That’s awesome, I’d like that. Are you on Twitter?

  • amazing job

    • Chris

      Thank you

  • Chris

    I don’t know of any way to suppress RCON log messages. You don’t get as many connection messages, if you keep an open connection, but the others (like placing blocks etc) still appear…

    • ceagle

      You can suppress messages like blocks-placed – I think, a gamerule did that in addition to suppressing them in op-chat.

      When keeping an RCON connection open, I had issues where it simply closed without notice and without any way of determining that it actually closed. Sometimes after 30 seconds, sometimes after less than a second. The server just stopped receiving those commands, unless I opened a new RCON connection. Due to that, I changed to opening it over and over.

Recommended
Sponsors
Get the latest in PHP, once a week, for free.