Key Takeaways
- Minecraft mods can be created without a deep knowledge of Java, using PHP instead. This is achieved through the use of asynchronous PHP libraries and an event loop, avoiding the need for traditional busy loops.
- AMPHP, an asynchronous PHP library, allows the use of promises for data not yet available. It uses generators to reduce the amount of code needed for iterating over values not yet defined in an array.
- Minecraft mods can listen to server logs to identify player commands. This can be used to execute mod commands and avoid executing the same command twice.
- A Minecraft mod can build large structures using a 3D JavaScript builder. The builder generates an array structure of the x, y, and z coordinates for each block placement.
- Communication with the Minecraft server is possible through an admin chat server (RCON). A pre-existing RCON client can be used to connect with the server and execute commands.
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) {
// 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:
- Call to
getContents
- Call to
$files->mtime($path)
(imagine this was just a proxy tofilemtime
) - Wait for
filemtime
to return - Call to
$files->get($path)
(imagine this was just a proxy tofile_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) {
// 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.
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;
}
// ...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.
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!
Frequently Asked Questions (FAQs) about Modding Minecraft with PHP
How can I start modding Minecraft with PHP?
Modding Minecraft with PHP is a fun and creative way to customize your gaming experience. To start, you need to have a basic understanding of PHP and Minecraft. You’ll also need a Minecraft server and the PHP programming language installed on your computer. Once you have these, you can start writing scripts to modify the game. For example, you can create buildings from code, which is a unique way to add structures to your Minecraft world.
What is the PHP-Minecraft-Query?
PHP-Minecraft-Query is a PHP library that allows you to gather information from Minecraft servers. It can retrieve details like the server’s version, online players, MOTD, and more. This library is useful for developers who want to integrate Minecraft server data into their PHP applications.
What is the difference between Minecraft Java and Bedrock editions?
Minecraft Java Edition is the original version of the game, designed for desktop and laptop computers, and it supports modding. On the other hand, the Bedrock Edition is designed for consoles, smartphones, and Windows 10. It has cross-play capabilities, allowing players on different platforms to play together, but it doesn’t support modding as extensively as the Java Edition.
Can I implement a Minecraft server in PHP?
Yes, it’s possible to implement a Minecraft server in PHP. However, it requires a good understanding of PHP and networking concepts. There are also projects like the PHP Minecraft Server, which is a Minecraft server implemented in PHP and powered by ReactPHP.
Can I play Minecraft on Windows?
Yes, Minecraft is available for Windows. You can purchase it from the Microsoft Store. The Windows version of Minecraft is part of the Bedrock Edition, which means it supports cross-play with other platforms like Xbox, PlayStation, and mobile devices.
What is the PHP-Minecraft/Minecraft-Query package?
The PHP-Minecraft/Minecraft-Query package is a PHP library that allows you to query Minecraft servers. It can retrieve information like the server’s version, online players, and more. This package is useful for developers who want to integrate Minecraft server data into their PHP applications.
How can I use PHP to create buildings in Minecraft?
You can use PHP to create buildings in Minecraft by writing scripts that generate the building blocks. These scripts define the size, shape, and materials of the building. Once the script is written, you can run it on your Minecraft server to create the building.
Can I mod Minecraft on consoles or mobile devices?
Modding is more limited on consoles and mobile devices compared to the Java Edition of Minecraft on PC. The Bedrock Edition, which is used on consoles and mobile devices, supports add-ons and the Minecraft Marketplace, where you can download user-created content. However, it doesn’t support extensive modding like the Java Edition.
How can I learn PHP for Minecraft modding?
There are many resources available online to learn PHP, including tutorials, online courses, and books. Once you have a basic understanding of PHP, you can start experimenting with Minecraft modding. You can also look at existing PHP scripts for Minecraft to learn how they work.
Can I share my PHP Minecraft mods with others?
Yes, you can share your PHP Minecraft mods with others. You can upload your scripts to a public repository like GitHub, or share them on Minecraft modding communities. However, make sure to respect the game’s terms of service and the rights of other players when sharing your mods.
Christopher is a writer and coder, working at Over. He usually works on application architecture, though sometimes you'll find him building compilers or robots.