minecraft源代码
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 mod。 可悲的是,我从不非常喜欢重新学习Java,这似乎一直是必需的。 直到最近。
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.
由于顽强的持久性,我实际上已经找到了一种在不真正了解Java的情况下制作Minecraft mod的方法。 有一些技巧和警告,可以让我们在舒适PHP中制作所需的所有mod。
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.
这只是冒险的一半。 在另一篇文章中,我们将看到一个简洁的3D JavaScript Minecraft编辑器 。 如果这听起来像您想学习的东西,请确保将其发布出去。
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.
该教程的大多数代码都可以在Github上找到。 我已经测试了最新版本的Chrome中的所有JavaScript位以及PHP 7.0中的所有PHP位。 我不能保证它在其他浏览器中看起来完全一样,或在其他版本PHP中也一样,但是核心概念是通用的。
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:
如您所见,我们将在PHP和Minecraft服务器之间传递负载。 只要需要mod的功能,我们就需要一个脚本来运行。 我们可以使用传统的繁忙循环:
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.
我已经很喜欢AMPHP了 。 它是异步PHP库的集合,包括HTTP服务器和客户端之类的内容以及事件循环。 如果您不熟悉这些东西,请不要担心。 我们会慢慢来。
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/fileThen, 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.
这与我们的无限循环类似,只是它是非阻塞的。 这意味着我们将能够执行更多的并发操作,同时等待通常会阻塞进程的操作。
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:
除了这个包装代码,AMPHP还提供了一个整洁的承诺为基础的界面。 您可能已经从JavaScript熟悉了这个概念,但是这里有一个简单的例子:
$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).
承诺是表示我们尚不具备的数据的一种方法-最终值。 这可能会很慢(例如文件系统操作或HTTP请求)。
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.
AMPHP 使用generators将承诺向前迈进了一步。 一口气解释这一切都有些激烈,但请耐心等待。
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
调用getContents
Call to $files->mtime($path) (imagine this was just a proxy to filemtime)
调用$files->mtime($path) (想象这只是filemtime的代理)
Wait for filemtime to return
等待filemtime返回
Call to $files->get($path) (imagine this was just a proxy to file_get_contents)
调用$files->get($path) (想象这只是file_get_contents的代理)
Wait for file_get_contents to return
等待file_get_contents返回
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:
我不认识你,但是这对我来说还是有点混乱。 那么发电机如何适应呢? 好吧,AMPHP使用yield关键字来评估promise。 让我们再次看一下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) 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.
$files->mtime($path)返回一个承诺。 该函数无需等待查找完成,而是会在遇到yield关键字时停止运行。 稍后,将通知AMPHP stat操作已完成,并且它将恢复此功能。
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).
然后,如果时间戳不匹配,则files->get($path)获取内容。 这是另一个阻塞操作,因此yield再次挂起该功能。 读取文件后,AMPHP将再次启动此功能(返回文件内容)。
This code looks similar to the synchronous alternative, but is using promises (transparently) and generators to make it non-blocking.
该代码看起来与同步替代方案相似,但使用的是Promise(透明地)和生成器以使其无阻塞。
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.
AMPHP与Promises A +规范有所不同,因为AMPHP承诺不支持then方法。 其他PHP实现,例如React / Promise和Guzzle Promises也可以。 重要的是要了解promise的最终性质,以及如何将它们与生成器进行接口,以支持这种简洁的异步语法。
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.
上次我写有关Minecraft的文章时 ,是关于使用Minecraft房屋的门来触发现实世界的警报。 在那篇文章中,我们简要介绍了如何将数据从Minecraft服务器中导出到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.
我们首先获取参考文件时间戳。 我们使用它来确定文件是否已更改(在getContents函数中)。 我们还创建一个空列表,在其中存储我们已经执行的所有命令。 此列表将帮助我们避免两次执行同一命令。
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.
您需要将/path/to/logs/latest.log替换为Minecraft服务器日志文件的路径。 我建议运行独立的Minecraft服务器 ,该服务器应将logs/latest.log放在根目录中。
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).
我们已经告诉Amp\repeat每500毫秒运行一次此关闭。 在那个时候,我们检查文件的变化。 如果时间戳已更改,我们会将日志文件的行拆分为一个数组并将其反转(以便我们首先读取最新的消息)。
If a line contains “> >” (as would happen if a player typed “> some command”), we assume that line contains a command instruction.
如果一行包含“>>”(如玩家键入“> some command”,就会发生这种情况),我们假设该行包含一条命令指令。
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.
Minecraft中最耗时的事情之一是建造大型结构。 如果我可以计划它们(使用一些时髦的3D JavaScript构建器),然后使用特殊命令将它们放置在世界中,将会容易得多。
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:
目前,该建造商仅允许放置污物块。 它生成的数组结构是放置的每个污垢块的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 } } }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.
每次收到命令时,都可以将其传递给executeCommand函数。 在那里,我们从第二个>提取到该行的末尾。 我们现在只需要识别build命令。
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.
听日志是一回事,但是我们如何与服务器通信呢? 独立服务器启动管理聊天服务器(称为RCON)。 这是在其他游戏(例如《反恐精英》)中启用mod的管理员聊天服务器。
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:
事实证明有人已经构建了RCON客户端(尽管有阻塞),最近我为此编写了一个不错的包装器。 我们可以通过以下方式安装它:
composer require theory/builderLet 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…
让我为那个图书馆有多大而道歉。 我提供了一个Minecraft独立服务器版本,以便可以为该库构建自动化测试。 太着急了...
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:
我们需要配置我们的独立服务器,以便我们可以与它建立RCON连接。 将以下内容添加到server.properties文件中,与服务器jar相同的文件夹中:
enable-query=true enable-rcon=true query.port=25565 rcon.port=25575 rcon.password=passwordAfter 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:
我们可以对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); } } }The new and improved executeCommand function checks to see if the command (a message resembling <player_name> > build) starts with the word “build”.
经过改进的新executeCommand函数检查命令(类似于<player_name> > build的消息)是否以单词“ 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(...).
如果构建器是非阻塞的,那么最好使用yield new Amp\Pause(500)而不是usleep(500000) 。 我们还需要将executeCommand视为生成器函数,在这里将其称为生成器函数,这意味着使用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.
如果是这样,该命令将被空格分隔,以获取应在其中构建设计的x , y和z坐标。 然后,它采用我们从设计人员生成的数组,并将每个块放置在世界上。
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.
您可能可以想象到我们刚刚创建的这个类似于mod的简单脚本的许多有趣的扩展。 可以扩展设计器以创建由许多不同种类和配置的块组成的布置。
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.
可以扩展mod脚本以通过JSON API接收更新,以便设计人员可以提交命名的设计,并且build命令可以确切指定播放器要构建的设计。
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!
我会将这些想法留给您练习。 不要忘记查看随附JavaScript帖子 ,如果您有任何想法或意见要分享,请在评论中进行!
翻译自: https://www.sitepoint.com/modding-minecraft-with-php-buildings-from-code/
minecraft源代码
相关资源:mcrcon:Minecraft的Rcon客户端-源码