我的世界服务器-MOD-构建指南-全-
我的世界服务器 MOD 构建指南(全)
原文:
zh.annas-archive.org/md5/339e027e61a63a7d942d79525e01cbdb译者:飞龙
前言
本书是使用 Bukkit API 编写 Minecraft 服务器插件的入门指南。Minecraft 是一个非常通用的沙盒游戏,玩家总是期待着用它做更多的事情。Bukkit 允许程序员做到这一点。本书面向可能没有编程背景的个人。它解释了如何设置 Bukkit 服务器并创建可以在服务器上运行的自己的自定义插件。它从 Bukkit 插件的基礎功能开始,如命令和权限,并继续深入到高级概念,如保存和加载数据。本书将帮助读者创建一个完整的 Bukkit 插件,无论他们是 Java 新手还是 Bukkit 新手。高级主题涵盖了 Bukkit API 的部分内容,甚至可以帮助当前插件开发者扩展他们的知识,以改进他们现有的插件。
本书涵盖的内容
第一章, 部署 Spigot 服务器,指导读者如何下载和设置一个运行在 Spigot 上的 Minecraft 服务器,包括转发端口以允许其他玩家连接。在这一章中,还解释了常见的服务器设置和命令。
第二章, 学习 Bukkit API,通过教授如何阅读其 API 文档来介绍 Bukkit。在这一章中,讨论了常见的 Java 数据类型和 Bukkit 类。
第三章, 创建您的第一个 Bukkit 插件,指导读者安装 IDE 并创建一个简单的“Hello World”Bukkit 插件。
第四章, 在 Spigot 服务器上测试,讨论了如何在 Spigot 服务器上安装插件以及简单的测试和调试技术。
第五章, 插件命令,指导如何通过创建一个名为 Enchanter 的插件来编程用户命令到服务器插件中。
第六章, 玩家权限,教授如何在插件中通过修改 Enchanter 来编程权限检查。这一章还指导读者安装一个名为 CodsPerms 的第三方插件。
第七章, Bukkit 事件系统,教授如何创建使用事件监听器的复杂 mod。这一章还通过创建两个新的插件,即 NoRain 和 MobEnhancer,帮助读者学习。
第八章,使您的插件可配置,通过扩展 MobEnhancer 教授读者程序配置。本章还解释了静态变量和类之间的通信。
第九章,保存您的数据,讨论了如何通过 YAML 文件配置保存和加载程序数据。本章还帮助创建了一个名为 Warper 的新插件。
第十章,Bukkit 调度器,在创建名为 AlwaysDay 的新插件时探讨了 Bukkit 调度器。在本章中,Warper 也被修改以包含计划编程。
您需要本书的内容
为了从本书中受益,您需要一个 Minecraft 账户。Minecraft 游戏客户端可以免费下载,但必须在minecraft.net/购买账户。本书中使用的其他软件包括 Spigot 服务器.jar(这与正常的 Minecraft 服务器.jar 不同)和一个 IDE,例如 NetBeans 或 Eclipse。本书将指导您在 Windows PC 上下载和安装服务器和 IDE 的过程。
本书面向对象
本书非常适合对自定义 Minecraft 服务器感兴趣的人。即使您可能对编程、Java、Bukkit 或甚至 Minecraft 本身都是新手,本书也能为您提供帮助。您所需的一切就是一个有效的 Minecraft 账户。本书是软件开发的优秀入门书籍。如果您对 Java 或编写软件没有任何先前的知识,您可以在codisimus.com/learnjava上找到一些入门教学,为阅读本书的章节做好准备。如果您对编程作为职业或爱好感兴趣,本书将帮助您入门。如果您只是想和朋友们一起玩 Minecraft,那么这本书将帮助您使这种体验更加愉快。
习惯用法
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:<java bin path> 应替换为 Java 安装的位置。Java 路径取决于您电脑上 Java 的版本。
代码块应如下设置:
"<java bin path>\java.exe" -jar BuildTools.jar
任何命令行输入或输出都应如下所示:
>op <player>
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“打开文件菜单并点击新建项目...。”
注意
警告或重要提示会以这样的框中出现。
小贴士
小技巧和窍门看起来像这样。
读者反馈
我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com的账户中下载示例代码文件,适用于您购买的所有 Packt 出版社的书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/BuildingMinecraftServerModificationsServerSecondEdition_ColorImages.pdf下载此文件。
错误更正
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误更正,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误更正提交表单链接,并输入您的错误更正详情来报告。一旦您的错误更正得到验证,您的提交将被接受,错误更正将被上传到我们的网站或添加到该标题的错误更正部分下的现有错误更正列表中。
要查看之前提交的错误更正,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍的名称。所需信息将出现在错误更正部分下。
侵权
在互联网上侵犯版权材料是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接将涉嫌盗版材料发送至 <copyright@packtpub.com>。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以联系 <questions@packtpub.com>,我们将尽力解决问题。
第一章:部署 Spigot 服务器
使用 Bukkit API 修改 Minecraft 的第一步是在你的 Windows PC 上安装一个多人服务器。一个多人服务器本质上与单人 Minecraft 服务器相同,但它允许更多的定制,并且不受限于你家里的网络。Spigot是 Minecraft 服务器的修改版本,它将用于加载你创建的插件。一个插件是一段软件,它可以连接或插入到另一段软件中。这本书中你将开发的代码将以插件的形式存在。这些插件将连接到 Minecraft 代码并改变 Minecraft 的操作方式。我们将设置一个 Spigot 服务器并使用它来测试你将编写的插件。到本章结束时,你的所有朋友都将能够登录到你的修改后的 Minecraft 服务器并一起玩耍。通过完成本章的以下部分,我们将部署一个 Spigot 服务器,它将在后面的章节中进行修改:
-
Spigot 简介
-
安装 Spigot 服务器
-
理解和修改服务器的设置
-
使用控制台和在游戏中的 Minecraft 和 Bukkit 服务器命令
-
端口转发
Spigot 简介
当你设置自己的服务器并开始创建插件时,你可能会遇到一些对你来说可能全新的术语。这些术语是Vanilla、Bukkit、CraftBukkit和Spigot。
Vanilla指的是由 Mojang/Microsoft 开发的正常 Minecraft 游戏。Vanilla 服务器是游戏的官方版本。可以从minecraft.net下载,通常命名为minecraft_server.jar或minecraft_server.exe。当前的 vanilla 服务器不支持任何类型的 mod 或插件。这就是 Bukkit 发挥作用的地方。
Bukkit是一个帮助我们开发插件的 API。我们将在第二章《学习 Bukkit API》中详细讨论。在此之前,只需知道当你听到bukkit plugins这个短语时,它指的是针对 Bukkit API 构建的插件。
Bukkit API 最初是由CraftBukkit团队开发的。这引出了下一个术语。CraftBukkit是一个修改版的 Minecraft 服务器,它取代了 vanilla 服务器。CraftBukkit 和 vanilla Minecraft 为我们提供了几乎相同的游戏。区别在于 CraftBukkit 具有加载 Bukkit 插件和执行游戏内代码的能力。CraftBukkit 将 Bukkit 方法和变量转换为 Mojang 开发的 Minecraft 代码。CraftBukkit 还包括额外的代码,以帮助插件开发者完成某些任务,例如保存/加载数据、监听服务器事件和安排需要执行的代码。在这本书中,我们将不会过多提及 CraftBukkit,因为它已被一个名为Spigot的项目所取代。
Spigot 完全替代了原版的 Minecraft 服务器,就像 CraftBukkit 一样。Spigot 是建立在 CraftBukkit 项目之上的。因此,它们共享了很多相同的代码。然而,Spigot 通过其设置提供了更多的可配置性;在许多方面,它运行得更快。Spigot 团队现在维护了所有三个项目,即 Bukkit、CraftBukkit 和 Spigot。你可以使用 CraftBukkit 或 Spigot 来运行服务器,因为 Spigot 团队已经足够友好地提供了这两个选项。我建议运行 Spigot 服务器,原因如前所述。
安装新的 Spigot 服务器
我们将从零开始设置这个新的服务器。如果你希望使用现有的世界,你将在创建新的 Spigot 服务器后能够这样做。首先,让我们创建一个名为 Bukkit Server 的新空文件夹。我们将从这个新创建的文件夹中运行 Spigot 服务器。
你需要启动新服务器的主体文件是 spigot.jar。一个 JAR 文件是一个可执行的 Java 文件。Minecraft、Spigot 以及我们将创建的每一个插件都是用 Java 编写的,因此它们都是从 JAR 文件中运行的。Spigot 团队会随着 Mojang 发布新的 Minecraft 版本来更新 spigot.jar 文件。通常情况下,当你连接到 Minecraft 服务器时,你必须玩的是同一个版本。如果你不确定你的 Minecraft 版本,它会在 Minecraft 客户端的左下角显示。一个 客户端 是你用来玩 Minecraft 的程序,如下面的截图所示:

你可以通过在 Minecraft 启动器中创建一个新的配置文件来选择你想要玩的 Minecraft 版本,如下面的截图所示:

由于法律原因,Spigot 团队不允许你下载 spigot.jar。然而,它确实提供了工具和说明,以便你可以自己构建 JAR 文件。Spigot 团队通过提供最新的说明以及故障排除指南来不断改进这一过程,指南可以在 www.spigotmc.org/threads/buildtools-updates-information.42865/ 找到。本章包括如何获取所需 JAR 文件的简化说明。然而,如果你在构建这些 jar 文件时遇到问题,请参考 spigotmc.org 上提供的说明。
为了运行构建工具,你需要 Git for Windows。你可以在msysgit.github.io/下载它。在安装 Git for Windows 时,默认的安装设置将足够。你还需要下载构建工具JAR文件,可以在hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar找到。创建一个新的文件夹来放置BuildTools.jar。将这个文件夹命名为Spigot Build Tools。在同一文件夹内创建一个新的文本文件。将这个文本文件命名为update.txt。打开这个text文件,并写入以下代码行:
"<java bin path>\java.exe" -jar BuildTools.jar
<java bin path> 应替换为 Java 安装的位置。Java 路径取决于你电脑上安装的 Java 版本。在Program Files或Program Files (x86)目录中查找Java文件夹。Program Files目录通常位于主硬盘的根目录,通常是C:\。如果你看不到 Java 文件夹,那么你可能需要安装 Java。你可以在java.com/download/下载它。
一旦你进入Java文件夹,你会看到一个或多个 java 安装文件夹,例如jre7或jre8。打开安装文件夹。如果有多个,请打开版本号较高的一个。在 java 安装文件夹中,打开bin目录。在这里你应该能看到java.exe,尽管它可能只显示为java。从资源管理器窗口的顶部复制路径;这就是 java bin 路径,如下面的截图所示:

如果你安装了 Java 8,那么更新文件中的代码行将类似于以下代码:
"C:\Program Files (x86)\Java\jre1.8.0_45\bin\java.exe" -jar BuildTools.jar
小贴士
在大多数 PC 上,你可以用 java 变量代替 java.exe 路径。因此,更新文件中的代码行将如下所示:java -jar BuildTools.jar
保存文件,并将其重命名为update.txt到update.sh。如果你看不到文件的.txt扩展名,那么你可能需要通过执行以下步骤来调整文件夹选项:
-
打开上左角的视图选项卡
-
选择文件夹和搜索选项
-
取消选中隐藏已知文件类型的扩展名
-
点击确定
现在,您可以将 update.txt 重命名为 update.sh。通过双击 update.sh 来运行它。这将执行构建工具,下载所有代码并应用更改,直到更新完成。这可能需要几分钟的时间。一旦完成,您将拥有 spigot.jar、craftbukkit.jar 和 bukkit.jar。两个服务器 JAR 文件,即 spigot 和 craftbukkit,将位于您放置 BuildTools.jar 的 Spigot Build Tools 目录中。bukkit.jar 文件位于同一文件夹中的 Bukkit/target 目录。每个文件都将附加一个版本号,例如 spigot-1.8.8.jar 和 bukkit-1.8.8-R0.1-SNAPSHOT.jar。请注意这些文件的位置,因为您在本章以及整本书中都需要它们。
注意
建议每周运行一次 update.sh 脚本,以确保您拥有 Spigot 的最新版本。
复制 spigot.jar 文件并将其放置在您在本段开始时创建的 Bukkit Server 文件夹中。为了简化,我们将移除版本号并将文件重命名为 spigot.jar。
现在,我们将创建一个批处理文件,每次我们想要启动服务器时都可以双击它。在一个新的文本文档中,输入以下两行:
java -Xms1024M -Xmx1024M -jar spigot.jar
PAUSE
1024 表示服务器将被允许使用的计算机 RAM 量。如果您想调整服务器使用的 RAM 量,可以更改此数字。spigot.jar 是 spigot.jar 文件的名字。这个名字必须与您的文件名匹配。我们将文件重命名以排除版本号,这样我们就不需要在每次更新 Spigot 服务器到最新版本时编辑此批处理文件。java 表示我们正在使用 Java 运行服务器。如果在接下来的步骤中服务器没有启动,您可能需要将 java 替换为之前复制的完整 Java 路径。批处理文件中的其余代码不应让您担心,并且应保持不变。
将文本文档保存为 Start Server.bat 并确保它与 spigot.jar 在同一文件夹中。现在,您将能够运行服务器。双击您刚刚创建的批处理文件。它将打开命令提示符并开始创建服务器文件。它看起来应该像下面的截图,并应显示您正在使用的 Minecraft 服务器版本:

如果您没有看到Starting minecraft server的消息,那么批处理文件可能存在问题。如果之前截图所示的窗口没有出现,请确保批处理文件名为Start Server.bat而不是Start Server.bat.txt。当您第一次启动服务器时,您将看到一些警告。其中大多数不必担心,因为它们是预期的。然而,您可能会看到一个解释您需要同意 EULA 才能运行服务器的消息。如果您查看Bukkit Server文件夹,您现在将看到一个名为eula.txt的新文件。打开此文件,将eula=true设置为同意条款,这些条款由 Mojang 在account.mojang.com/documents/minecraft_eula中概述。一旦这样做,您就可以再次启动服务器。这次,您将看到服务器正在加载并生成一个新的世界。
设置新服务器
您将看到服务器文件夹中填充了几个文件和文件夹。其中一些文件的作用在本节中解释,但大多数文件目前不应引起您的关注:
-
plugins: 此文件夹是您将放置所有希望用于服务器的 Bukkit 插件的文件夹。 -
world: 以world开头的文件夹,例如world、world_nether等,包含服务器新世界的所有信息。如果您已经有一个希望使用的 Minecraft 世界,那么请用旧世界文件夹替换这些新文件夹。不要在服务器运行时尝试这样做,因为这会导致问题。 -
server.properties: 此文件包含多个选项,允许您更改 Minecraft 服务器的运行方式。您可以使用文本编辑器打开它。有许多设置,其中大多数都很直观。以下列表中我将介绍一些您可能想要修改的设置。要查看属性解释的完整列表,您可以访问www.minecraftwiki.net/wiki/Server.properties。更改这些设置将需要您重新启动服务器。-
pvp=true:pvp属性可以被设置为布尔值。PvP(即玩家对玩家)决定玩家是否可以攻击并伤害彼此。您可以根据是否希望开启或关闭 PvP 功能,将其设置为true或false。 -
difficulty=1: 难度属性可以被设置为从0到3的数字,其中0表示和平,1表示简单,2表示普通,3表示困难。服务器上的每个人都将在这个难度级别上玩游戏。 -
gamemode=0: 此属性确定玩家将开始哪种游戏模式,其中0表示生存,1表示创造,2表示冒险。 -
motd=A Minecraft Server: MOTD(即每日消息。当你在 Minecraft 多人服务器列表中查看服务器时,将显示此消息,如下面的截图所示。将此设置为对服务器简短描述是个好主意。例如,可以是Bukkit 插件测试。设置新服务器 -
online-mode=true: 可以将此设置为false,以允许玩家在离线模式下连接到服务器。如果minecraft.net/不可用或你的电脑未连接到互联网,这很有用。在离线模式下运行你的服务器可能会引起安全问题,例如其他玩家登录到你的账户。
-
-
bukkit.yml: 此文件包含许多服务器选项。这些是原版服务器不提供的选项,并且仅在运行修改后的服务器时可用。请注意,此文件是 YMAL(.yml)文件,而不是属性文件(.properties)。当你打开它时,你会看到两种文件类型格式不同。你首先会看到的是某些行有缩进。你不需要完全理解 YMAL 格式,因为随着我们创建 Bukkit 插件的过程,它将会进一步解释。此文件中有一些设置我将引起你的注意,如下面的列表所示。要查看这些 Bukkit 设置的完整列表,你可以访问wiki.bukkit.org/Bukkit.yml。与server.properties一样,更改这些设置将需要你重新启动服务器。-
allow-end: true: 原版 Minecraft 服务器允许你禁用下界世界的功能。Bukkit 服务器也允许你禁用末地世界。将此设置为false以防止玩家前往末地。 -
use-exact-login-location: false: 原版 Minecraft 包含一个功能,该功能将阻止玩家在方块内生成。玩家将生成在方块上方,这样他们在加入服务器时就不会卡住。这可以很容易地被用来爬上玩家通常无法触及的方块。Bukkit 可以通过在玩家注销的确切位置生成玩家来防止这种情况发生。如果你希望防止这种情况,请将此属性设置为true。 -
spawn-limits: Bukkit 允许服务器管理员修改在给定区块内允许生成的怪物和动物的数量。如果你不熟悉区块这个术语,它是从床岩到天空最高点的一组16 x 16方块。以下是在 Minecraft 中单个区块的图片;如果你觉得生成的怪物太多(或太少),那么你将想要相应地调整这些值:设置新服务器 -
ticks-per: autosave: 0: 与 vanilla Minecraft 不同,Bukkit 服务器不会定期保存你的玩家/世界数据。自动保存数据可能会防止服务器在崩溃或由于某些原因(如电脑断电)关闭时丢失游戏中所做的更改。vanilla 默认将其设置为6000。这个值是以tick为单位的。每秒有 20 个tick。我们可以用这个数学方法来确定 6,000 个 tick 是多长时间:6000 tick / 20 tick/second = 300 秒,300 秒 / 60 秒/分钟 = 5 分钟。从这个计算中,你应该能够计算出你希望服务器在多长时间后自动保存你的进度。如果你的服务器在保存更改时出现延迟,那么你可能想增加这个数字。设置为72000将导致每小时只出现一次延迟。然而,如果服务器在即将保存之前崩溃,你可能会丢失过去一小时所做的任何进度。
-
-
spigot.yml:这个文件类似于bukkit.yml。它包含许多仅在运行 Spigot 服务器时才可用的设置和配置。如果你希望配置这些选项中的任何一个,请参阅文件顶部的文档。
Minecraft/Bukkit 服务器命令
我们现在已经设置了所有自定义选项。接下来,让我们登录到服务器并查看游戏中的服务器命令。
要登录到你的服务器,你需要知道你电脑的 IP 地址。在本章的后面部分,我们将讨论如何找到这个必要的信息。然而,现在我将假设你将在运行你的服务器的同一台机器上玩 Minecraft。在这种情况下,对于服务器的 IP,只需输入localhost。一旦你连接到服务器,你会看到 Spigot 服务器本质上与 vanilla 服务器相同,因为你还没有安装任何插件。服务器运行 Bukkit 的第一个迹象是你将有一些额外的命令可供使用。
Bukkit 继承了所有 Minecraft 服务器命令。如果你曾经玩过 Minecraft 服务器,那么你可能已经使用过其中的一些命令。如果你还没有,我将解释一些有用的命令。这些命令可以输入到控制台或游戏内控制台。在这里,“控制台”指的是运行你的服务器的命令提示符。Bukkit 有一个内置的权限系统,它限制了玩家使用特定命令。
如果他们没有必要的权限,他们将无法使用命令。我们将在后面的章节中详细讨论这个问题,但到目前为止,我们将使你的玩家成为操作员,或简称op。操作员自动拥有所有权限,并将能够执行所有将要展示的命令。要成为操作员,将op命令输入到控制台,如下所示:
>op <player>
将<player>替换为你的玩家名称。请参见以下截图中的高亮命令以获取示例:

一旦你被 Opped,你就可以测试一些游戏内的服务器命令了。为了正确使用命令,你必须了解命令语法。以下以 gamemode 命令为例:
gamemode <0 | 1 | 2 | 3> [player]
-
< >表示这是一个必需的参数。 -
[ ]表示这是一个可选参数。对于此命令,如果未包含玩家参数,则将设置你自己的游戏模式。 -
|是表示单词 或 的已知符号。因此,<0 | 1 | 2 | 3>表示可以输入 0、1、2 或 3。它们分别代表 生存模式、创造模式、冒险模式 和 旁观者模式。 -
参数必须始终按照显示的顺序输入。通常,如果你输入了错误的命令,会出现一条帮助信息,提醒你如何正确使用该命令。
注意,当你发布游戏内命令时,必须以 / 开头,但当你从控制台发布命令时,/ 必须省略。正确使用 gamemode 命令将是 /gamemode 1,这将把你的游戏模式设置为创造模式,如下面的截图所示:

这个命令的另一个示例是 /gamemode 2 Steve,这将找到用户名为 Steve 的玩家并将他的游戏模式更改为冒险模式。
现在你已经了解了命令的基本语法,你可以从以下列表中学习如何使用一些其他有用的服务器命令。其中大部分命令也存在于原版 Minecraft 中。只有少数命令是 Bukkit 服务器特有的:
-
gamerule <rule> [true | false]这种示例是
/gamerule mobGriefing false。规则参数可以设置为以下任何一个:
-
doMobSpawning: 这决定了怪物是否会自然生成 -
keepInventory: 这决定了玩家死亡时是否保留他们的物品 -
mobGriefing: 这决定了像爬行者这样的怪物是否可以破坏方块 -
doFireTick: 这决定了火是否应该蔓延 -
doMobLoot: 这决定了怪物是否应该掉落物品 -
doDaylightCycle: 这决定了日夜循环是否生效
-
-
give <player> <item> [amount [data]]- 例如,
/give Codisimus wool 3 14给 Codisimus 3 红色羊毛。
- 例如,
-
plugins(仅适用于 Bukkit)- 例如,
/plugins 或 /pl显示服务器上安装的所有插件列表。
- 例如,
-
reload(仅适用于 Bukkit)- 例如,
/reload或/rl禁用所有插件并重新启用它们。此命令用于加载插件的新设置,而无需关闭整个服务器。
- 例如,
-
spawnpoint [player] [x y z]- 例如,
/spawnpoint允许你在死亡时出现在你站立的位置。
- 例如,
-
stop- 例如,
/stop保存你的进度并关闭服务器。这是你应该停止服务器以确保数据保存的方法。如果你只是通过点击 X 关闭命令提示符,你将丢失数据。
- 例如,
-
tell <player> <message>- 例如,
/tell Steve my secret base is behind the waterfall发送一条只有 Steve 能看到的消息。请注意,这些消息也会打印到控制台。
- 例如,
-
time set <day | night>- 例如,
/time set day将服务器的时钟设置为0(白天)。
- 例如,
-
toggledownfall- 例如,
/toggledownfall停止或开始降雨/降雪。
- 例如,
-
tp [player] <targetplayer>- 例如,
/tp Steve Codisimus将 Steve 传送到 Codisimus 的位置。
- 例如,
关于这些和其他命令的更多信息,请访问 minecraft.gamepedia.com/Commands 和 wiki.bukkit.org/CraftBukkit_commands。前面提到的命令和属性文件给了您很多控制服务器功能的方式。
端口转发
当没有其他人可以登录时,自己运行 Minecraft 服务器有什么乐趣呢?我现在将解释如何允许您的朋友连接到您的服务器,这样他们就可以和您一起玩了。为了做到这一点,我们首先需要找到您的 IP 地址。就像您的住所有一个街道地址一样,您的计算机在互联网上也有一个地址。这就是您的朋友将在他们的 Minecraft 客户端中输入以找到您的服务器的地址。要找到 IP 地址,只需在 Google 上搜索 IP。在结果顶部将有一条声明, "您的公共 IP 地址是 XX.XX.XXX.XX”(X 符号将被数字替换,其总长度可能不同)。您还可以访问 www.whatismyip.com 来查找您的 IP 地址。
一旦您有了 IP 地址,尝试使用它来连接到您的服务器,而不是使用 localhost。如果您能够连接,那么您的朋友也能做到。如果不能,您将需要采取额外的步骤来允许其他玩家连接到您的服务器。如果您的计算机连接到路由器,情况就是这样。我们必须让路由器知道它应该将其他 Minecraft 玩家指向运行服务器的您的计算机。这个过程称为 端口转发。为此,我们首先需要一些额外的信息。
我们需要知道您在本地网络中的计算机的 IP 地址。这个 IP 地址将与我们之前获得的不同。我们还需要知道您路由器的 IP 地址。要获取这些信息,请打开一个新的命令提示符窗口。命令提示符可以在以下路径找到:
开始菜单/所有程序/附件/命令提示符
您也可以搜索 cmd.exe 来找到它。一旦命令提示符打开,输入以下命令:
>ipconfig
然后,按 Enter 键。将显示一个屏幕,它将类似于以下截图所示:

在上一张图片中,您要找的两个 IP 地址已经被突出显示。这些数字很可能与这些示例数字非常相似。"IPv4 地址" 是您的计算机地址,而 "默认网关**" 是您路由器的地址。请记下这两个 IP 地址。
接下来,我们将登录到您的路由器。在网页浏览器中,输入路由器的 IP 地址,在我们的例子中是 192.168.1.1。如果您这样做正确,那么您将看到一个登录表单,要求输入用户名和密码。如果您不知道这些信息,您可以在两个字段中尝试输入 admin。如果这不起作用,您将不得不找到默认的用户名和密码,这些信息通常可以在您的路由器提供的文件中找到。这些信息通常也可以通过搜索您的路由器名称以及 "默认登录**" 等术语在网上找到。
一旦我们登录到路由器,我们必须找到包含端口转发设置的区域。世界上有各种各样的路由器品牌和型号,它们都以不同的方式提供此选项。因此,我无法提供如何找到此页面的具体说明。但是,您会想要寻找一个标签,其名称包含 "转发"、"端口转发" 或 "应用与游戏**" 等术语。如果您看不到这些选项,请通过探索高级设置来扩大搜索范围。一旦找到正确的页面,您很可能会看到一个类似于以下表格的表格:
| 应用名称 | 外部端口 | 内部端口 | 协议 | IP 地址 |
|---|---|---|---|---|
| Bukkit 服务器 | 25565 | 25565 | TCP 和 UDP | 192.168.1.100 |
按照上一张表格所示填写字段。布局和格式当然会根据您的路由器而有所不同,但重要细节是您需要将端口 25565 转发到您之前找到的 IP 地址,在我们的例子中是 192.168.1.100。确保保存这些新设置。如果您操作正确,那么现在您应该能够通过使用您的公共 IP 地址连接到服务器。
摘要
您现在已经在您的 PC 上运行了一个 Spigot 服务器。您可以告诉您的朋友您的 IP 地址,这样他们就可以和您一起在您的新服务器上玩游戏。在本章中,您熟悉了游戏内命令及其使用方法,并且一旦我们编写了插件,您的服务器就可以安装 Bukkit 插件。为了准备编写这些插件,我们首先需要熟悉 Bukkit API 以及如何使用它。
第二章:学习 Bukkit API
在本章中,您将了解Bukkit API,并学习通过为 Spigot 服务器编写插件编程可以完成什么。到本章结束时,您可能会有很多关于插件的想法,您最终将能够自己创建它们。本章将详细介绍以下主题:
-
理解 API 的目的
-
查找 Bukkit API 的文档
-
在 Javadocs 中导航以查找特定信息
-
阅读和理解文档
-
探索和学习 Bukkit API 的功能
API 简介
API代表应用程序编程接口。API 帮助控制各种软件组件的使用方式。正如前一章所述,Spigot 将 Minecraft 代码以开发者易于利用的形式包含在内,以便在创建插件时使用。Spigot 包含大量我们不需要访问的代码,以便创建插件。它还包括我们不应篡改的代码,因为这可能会导致服务器变得不稳定。Bukkit 为我们提供了可以用来正确修改游戏并限制对代码其他部分的访问的接口。接口本质上是一个类的壳。它包括方法,但方法是空的。Spigot 服务器包含每个接口的类。这些类实现了接口,并用适当的代码填充每个方法。
为了更好地解释这一点,让我们想象 Bukkit API 就像一家披萨店的菜单。菜单包含不同类型的披萨,如意大利辣肠披萨、夏威夷披萨和肉食爱好者披萨。这些菜单项代表 API 中的接口,每个接口都有一个名为makePizza的方法。在这个阶段,这些披萨不能吃,因为它们只是一个概念。它们只是菜单上的项目。但假设一家名为“所有你需要的是披萨”的披萨店决定开业,并使用这个菜单或 API。这家披萨店可以代表 CraftBukkit。披萨店为菜单上的每个项目创建食谱。这相当于为三个接口中的每个makePizza方法编写代码。因此,这些食谱是实现接口的类。然而,这些类仍然只是一个概念。只有在调用makePizza方法时,您才有这个类的实例。这个实例,或对象,将是您可以真正食用的有形披萨。现在,想象另一家名为“疯狂的小东西叫披萨”的披萨店在“所有你需要的是披萨”的对面开业。这家新披萨店将代表 Spigot。“疯狂的小东西叫披萨”使用与“所有你需要的是披萨”完全相同的菜单或 API。然而,它的食谱,或方法的实现,可能不同。
使用这个相同的类比,我们可以看到 API 的好处。作为一个客户,我可以查看菜单并组装一个订单。例如,我想点一个辣味披萨和一个肉食爱好者披萨。由于我的订单是基于菜单,并且两家披萨店都实施了相同的菜单,所以任何一家餐厅都能满足我的订单。同样,开发者基于 Bukkit API 创建插件。CraftBukkit 和 Spigot 都使用 Bukkit API。因此,它们都将支持该插件。以下图表解释了披萨和代码之间的这种关系:

基本上,Bukkit 在插件和 Spigot 服务器之间充当桥梁。随着 Minecraft 中新功能的开发,Spigot 团队会向 API 中添加新的类、方法等,但现有的代码很少改变。这确保了即使 Minecraft/Spigot 发布了新版本,Bukkit 插件仍然可以正确运行数月甚至数年。例如,如果 Minecraft 改变了实体生命值处理的方式,我们不会看到任何差异。
Spigot jar 会通过将更新后的代码填充到 getHeath 方法中来处理这种变化。然后,当插件调用 getHealth 方法时,它将像更新前一样正常工作。新 Minecraft 功能(如新物品)的添加是 Bukkit API 优秀的另一个例子。假设我们创建了一个给食物添加保质期的插件。要检查一个物品是否是食物,我们将使用 isEdible 方法。Minecraft 继续创建新物品。如果这些新物品中有一个是 南瓜面包,Spigot 将将该类型物品标记为可食用,因此我们的插件将为其设置保质期。一年后,即使我们不需要更改任何代码,新的食物物品仍然会被赋予保质期。
Bukkit API 文档
Bukkit API 的文档可以在 hub.spigotmc.org/javadocs/bukkit/ 找到。你在 第一章 中构建的 Bukkit.jar 文件,部署 Spigot 服务器 也包含了 Spigot API,可以在 hub.spigotmc.org/javadocs/spigot/ 找到。Spigot API 是 Bukkit API 的 超集,这意味着它包含了在 Bukkit API 中存在的所有类、接口等,以及一些仅属于 Spigot 项目的独特类。如果你想你的插件支持 Spigot 和 CraftBukkit 服务器,那么你将希望使用 Bukkit API 进行开发。如果你选择只支持 Spigot 服务器,那么你可以使用 Spigot API 进行开发。在这本书中,我们将参考 Bukkit API。然而,使用 Spigot API 将产生相同的结果。
在 Bukkit API 文档中导航
我们可以通过 Bukkit API 文档来了解我们可以在 Spigot 服务器上修改什么。服务器端插件与客户端模组不同,因为我们使用服务器端插件在游戏中修改的能力有限。例如,我们无法创建新的方块类型,但我们可以让熔岩方块从天空中落下。我们无法让僵尸看起来和听起来像恐龙,但我们可以给僵尸套上绳索,将其名字改为 Fido,并让它不在白天燃烧。大部分情况下,你无法改变游戏的外观,但你可以在功能上做出改变。这确保了所有使用标准 Minecraft 客户端连接到服务器的玩家都将有相同的体验。
为了了解更多我们可以做什么的例子,让我们看看 API 文档的各个页面:

你会看到 API 中的类和接口在Javadoc的左下角是可选的。在左上角选择一个包将缩小下面部分的选项。每种类型,如类或接口,都组织在一个包中。这些包有助于将类似的类分组在一起。例如,Cow、Player和Zombie都是实体类型,因此可以在org.bukkit.entity包中找到。所以,如果我说World接口可以在org.bukkit.World找到,那么你就会知道你可以在org.bukkit包中找到World。了解这一点将帮助你找到你正在寻找的类或接口。你始终可以使用Ctrl + F在网页上搜索特定的单词。这有助于在长列表中找到特定的类。
让我们来看看World类,看看它有哪些功能。类按字母顺序列出。因此,我们将在org.bukkit包的末尾找到World类。当你点击World类的链接时,所有的方法将在网站主列中的方法摘要标题下显示,如下面的截图所示:

World对象是服务器上的整个世界。默认情况下,Minecraft 服务器有多个世界,包括主世界、下界世界和末地世界。Spigot 甚至允许你添加额外的世界。World类中列出的方法可以应用于特定的世界对象。例如,Bukkit.getWorlds方法将给你一个服务器上所有世界的列表;每个都是唯一的。因此,如果你在第一个世界上调用的getName方法可能返回world,而在第二个世界上调用的相同方法可能返回world_nether。
理解 Java 文档
让我们看看 World 类中包含的一个方法,看看它提供了哪些信息。点击链接查看 createExplosion(Location loc, float power, boolean setFire) 方法。你将被带到与以下截图类似的方法描述:

截图解释了该方法每个参数和返回值。此方法需要我们传递三个参数,具体解释如下:
-
爆炸应该发生的位置
-
爆炸应该有多强大
-
是否爆炸应该导致周围的方块着火
如果返回值是 void,则该方法不会向我们发送任何信息。在这个例子中,该方法返回一个 boolean 值。在阅读文档时,你会了解到返回值表示爆炸是否实际发生。如果另一个插件阻止了爆炸的发生,那么 createExplosion 方法将返回 false。
探索 Bukkit API
现在你已经熟悉了 Bukkit API 文档,我建议你自己浏览一下。你会发现有趣的方法;其中许多方法会激发你制作酷炫插件的灵感。请注意,可能会有额外的链接查看该对象的其他方法。例如,Player 是 **LivingEntity** 类型的一种。因此,你可以在 Player 对象上调用 **LivingEntity** 方法。这种继承关系在方法摘要之后显示,如下面的截图所示:

如果你打算尝试想出一个插件的想法,浏览 API 文档肯定会给你一些灵感。我建议阅读以下列出的类页面,因为它们将是你在未来插件中经常使用的类:
| 类 | 包 | 描述 |
|---|---|---|
World |
org.bukkit |
服务器上的一个世界 |
Player |
org.bukkit.entity |
在服务器上玩的人 |
Entity |
org.bukkit.entity |
玩家、怪物、物品、投射物、车辆等 |
Block |
org.bukkit.block |
世界中的特定块,例如泥土块或箱子 |
Inventory |
org.bukkit.inventory |
玩家、箱子、熔炉等的库存 |
ItemStack |
org.bukkit.inventory |
存储在库存中的物品,包括物品的数量 |
Location |
org.bukkit |
实体或方块的位置 |
Material |
org.bukkit |
块或物品的类型,例如 DIRT、STONE 或 DIAMOND_SWORD |
Bukkit |
org.bukkit |
包含许多可以在代码的任何地方调用的有用方法 |
现在你已经了解了如何阅读 Bukkit Java 文档,你可以找到你可能有的各种问题的答案。例如,如果你想找出调用哪些方法来获取名为"world"的世界中x:20 y:64 z:14位置的 Block,你会怎么做?
首先,你需要检索正确的World对象。你可能检查的第一个地方是 Bukkit 类,如前表所示。你可以检查那里,因为你可以从你的代码的任何地方调用这些方法。另一个选项是查看World类的使用情况。这可以通过点击World页面顶部的Use链接来完成。在那里,你可以看到所有返回World对象的方法以及接受World对象作为参数的方法。为了帮助你在页面上搜索,请记住你可以使用Ctrl + F。搜索name将带你到Bukkit.getWorld方法,该方法接受世界的名称作为参数并返回实际的World对象。
一旦你有了World对象,你将想要找到一个方法来获取特定位置的Block。你可以导航到世界页面,并使用Ctrl + F来搜索block、location、x、y或z。如果这些都没有帮助你找到有用的方法,那么你总是可以以类似于我们查看 World 使用方式的方式来查看 Block 的使用。无论如何,你都会找到World.getBlockAt方法,这个方法可以在你上一步发现的World对象上调用。
以下是一些额外的挑战,以指导你在自己探索 Bukkit API 并熟悉它时:
-
你会调用哪个方法来检查一个世界中的时间是什么时候?
-
你会调用哪些方法来向名为 Steve 的玩家发送消息?
-
你会调用哪些方法来检查一个块的材质是否可燃?
-
你会调用哪个方法来检查玩家是否在他们的库存中有钻石?
-
你会调用哪些方法来检查玩家是否持有可食用的物品?
摘要
如果你在解决挑战中提到的任何问题或 Bukkit API 的任何其他部分遇到困难,你可以从 Spigot 论坛(www.spigotmc.org/forums)、Spigot 的官方 IRC 频道(www.spigotmc.org/pages/irc)和 Minecraft 论坛(www.minecraftforum.net)寻求帮助。
你也可以直接联系我或访问我的网站www.codisimus.com。我总是乐于帮助其他开发者。
你现在拥有了开始编写自己的 Bukkit 插件所需的知识。正如我们在本章中所做的那样,我们将不得不参考文档来查找所需的信息。能够导航并理解 API 文档将加快编码过程。如果你对 API 的某个部分感到不确定,你现在知道如何找到你需要的信息。在下一章中,我们将使用 Bukkit API 来开始编写代码,创建你的第一个 Bukkit 插件。
第三章:创建您的第一个 Bukkit 插件
我们将要编写的 Bukkit 插件将使用 Java 编程语言编写。我们将使用IDE(集成开发环境的缩写)来编写插件。IDE 是一种软件,它将帮助我们编写 Java 代码。它有许多工具和功能,使编程变得更容易。例如,它自动检测代码中的错误,它经常告诉我们如何修复这些错误,有时甚至为我们修复,它还提供了许多快捷方式,例如编译代码和构建 JAR 文件的按键,以便代码可以执行。在本章中,我们将下载和安装 IDE,并准备创建一个新的 Bukkit 插件。我们将涵盖以下主题,并在本章结束时,我们将编写我们的第一个插件,它将准备好在我们的服务器上进行测试:
-
安装 IDE
-
创建新项目
-
将 Bukkit 添加为库
-
plugin.yml文件 -
插件的
main类 -
创建和调用新方法
-
扩展代码
安装 IDE
在这本书中,我们将使用 NetBeans 作为我们的 IDE。还有其他流行的 IDE,例如 Eclipse 和 IntelliJ IDEA。如果您愿意,可以使用不同的 IDE。然而,在本章中,我们将假设您正在使用 NetBeans。无论您选择哪个 IDE,Java 代码都将相同。因此,只要您正确设置代码,您就可以在剩余的章节中使用任何 IDE。如果您对编程相当新手,那么我建议您现在使用 NetBeans,在您对编程更加熟悉之后,尝试其他 IDE,并选择您喜欢的 IDE。
NetBeans IDE 可以从www.oracle.com/technetwork/java/javase/downloads/下载。从 Oracle 下载程序也将允许我们同时下载所需的Java 开发工具包(JDK)。您将看到几个下载链接。点击 NetBeans 链接访问JDK 8 with NetBeans下载页面。一旦您选择接受许可协议,您就可以下载软件。下载链接位于一个类似于以下截图所示的表格中:

如果您的 PC 运行的是 64 位 Windows 操作系统,那么您将想要使用对应于Windows x64的链接。如果您的 PC 运行的是 32 位 Windows 操作系统,或者您不确定是否是 64 位或 32 位 Windows 操作系统,那么请下载Windows x86版本。
小贴士
如果您想检查您是否正在运行 64 位版本的 Windows,您可以通过查看控制面板中的系统窗口来检查。
下载完成后,安装软件。在安装过程中,你可能会被问及是否想要安装JUnit。我们不会使用JUnit。因此,你应该选择不安装 JUnit。在安装程序的下一几个屏幕中,你将被询问希望安装两种类型软件的位置。默认设置是合适的。你可以简单地点击下一步。
创建新项目
安装完成后,打开 NetBeans 开始创建第一个项目。你可以通过以下步骤创建一个新项目:
-
打开文件菜单并点击新建项目...。
-
我们想要创建一个新的Java 应用程序。默认情况下已经选中了Java Application。因此,只需点击下一步。
-
我们现在需要命名第一个项目。避免在名称中使用空格是一个好主意。让我们将这个项目命名为
MyFirstBukkitPlugin。 -
除非你想要将你的项目存储在另一个位置,否则你可以保留项目位置的默认值。
-
确保已勾选创建主类。
main类是我们将放置所需代码以启用我们想要创建的插件的地方。对于这个字段,你必须确定你的项目包。这通常涉及到你的网站域名以相反的顺序。例如,Bukkit 使用org.bukkit,而我使用com.codisimus。假设你没有自己的域名,你可以使用你的电子邮件地址,例如com.gmail.username。你需要使用一些独特的东西。如果两个插件有相同的包,可能会导致类名冲突,Java 将无法知道你指的是哪个类。使用你拥有的电子邮件地址或域名是一个确保其他开发者不使用相同包的好方法。出于同样的原因,你应该将bukkit或minecraft排除在你的包名称之外。包名称也应该像前面的例子一样全部小写。
一旦有了包,你需要给你的主类命名。为了避免混淆,大多数 Bukkit 插件开发者使用项目名称作为主类名称。主类的名称应该以大写字母开头。
以下截图展示了点击完成之前你的表单应该呈现的样子:

将 Bukkit 作为库添加
现在我们已经创建了主类,我们需要将 Bukkit API 作为项目的库添加。你可能还记得,在上一章讨论过,API 包括我们可以访问以修改 Spigot 服务器的代码。你在第一章中构建 Spigot jar 时构建了 API JAR 文件,部署 Spigot 服务器。如果需要,请参考此章节以检索Bukkit.jar文件。你可能希望将其移动到更永久的位置。我建议你创建一个名为Libraries的文件夹并将 JAR 文件放在这个文件夹中。文件名可能附加了一个版本号。我们将重命名此文件,这与我们对spigot.jar所做的一样。这将帮助我们将来更容易地更新它。因此,bukkit.jar文件的新位置将类似于C:\Users\Owner\Documents\NetBeansProjects\Libraries\bukkit.jar。请记住你的文件位置,因为我们现在有了 Bukkit API,我们可以在 NetBeans 中为其创建一个库。
在 NetBeans 中,在项目选项卡内,你会看到一个库文件夹。当你右键单击它时,你会看到一个添加库...选项。点击它以显示当前库的列表,如下面的截图所示:

对于第一个项目,我们需要通过以下步骤创建 Bukkit 库。对于未来的项目,它将已经存在,我们只需选择它即可:
-
点击创建...并输入Bukkit作为库名称。
-
在下一个窗口中,有一个添加 JAR/Folder...按钮。点击它以定位并添加
bukkit.jar文件。 -
保持源选项卡为空,然后点击Javadoc选项卡。
-
在此选项卡中添加
hub.spigotmc.org/javadocs/spigot/并点击确定。这允许我们直接在 IDE 中阅读 API 文档的一些部分。
现在,你将能够选择Bukkit作为库并将其添加到项目中。
注意
注意,为了更新到 Bukkit 的新版本,你只需用新版本替换当前的bukkit.jar文件,就像你更新服务器上的spigot.jar文件一样。不需要对你的现有项目进行任何额外的修改。然而,你必须编译这些项目中的代码以检查是否有任何新的错误出现。
Bukkit 插件的基本要素
每个 Bukkit 插件都需要两个特定的文件。这些文件是plugin.yml和插件的主类。我们将从创建这些文件的最低版本开始。你未来的所有项目都将从创建这两个文件开始。
plugin.yml 文件
我们已经准备好开始编写 Bukkit 插件了。我们将创建的第一个文件是plugin.yml。这是 Spigot 服务器读取以确定如何加载插件的文件。在源包上右键单击,然后点击新建 | 其他...,如下面的截图所示:

在出现的窗口中,在类别下选择其他。然后,在文件类型下选择YAML 文件,如下面的截图所示,然后点击下一步:

将文件名设置为plugin,让文件夹的名称为src,然后点击完成。现在,你的项目结构应该如下面的截图所示:

plugin.yml文件是在默认包中创建的。这就是它需要的位置,以便 Spigot 可以找到它。现在,我们将用最基本的设置填充plugin.yml文件。plugin.yml文件必须包含你插件的名称、版本和主类。我们已经确定了名称和主类,我们将给它一个版本号 0.1。
小贴士
如果你想要了解更多关于版本号的信息,维基百科上有一篇关于这个主题的优秀文章,链接为en.wikipedia.org/wiki/Software_versioning。
plugin.yml的最简单形式如下:
name: MyFirstBukkitPlugin
version: 0.1
main: com.codisimus.myfirstbukkitplugin.MyFirstBukkitPlugin
这就是在这个文件中你需要的所有内容,但你可以添加一些其他字段,比如author、description和website。我们已经完成了这个文件。你可以保存并关闭plugin.yml。
插件的主类
我们需要修改主类。如果MyFirstBukkitPlugin.java文件还没有打开,请打开它。在插件中我们不使用main方法。因此,我们将删除该代码段。现在,你将拥有一个空白的 Java 类,如下面的代码所示:
package com.codisimus.myfirstbukkitplugin;
/**
*
* @author Owner
*/
public class MyFirstBukkitPlugin {
}
小贴士
你可能会看到一些额外的注释,但它们不会影响程序的执行。它们的存在是为了帮助任何可能阅读代码的人理解它。总是对所写的代码进行注释是一个好主意。如果有人最终阅读了你的代码,无论是其他开发者还是一周后的你自己,他们都会感谢你添加了注释。
我们需要做的第一件事是告诉 IDE 这个类是一个 Bukkit 插件。要做到这一点,我们将在类名后立即添加extends JavaPlugin来扩展JavaPlugin类。修改后的行将如下所示:
public class MyFirstBukkitPlugin extends JavaPlugin {
你将看到一条波浪线和一个小灯泡出现。这经常会发生,通常意味着你需要从 Bukkit API 中导入一些内容。如果你要求 IDE 这样做,它会为你完成。点击小灯泡并从 Bukkit 库中导入JavaPlugin,如下面的截图所示:

这将自动在类顶部附近添加一行代码。目前,你可以在服务器上安装此插件,但它当然不会做任何事情。让我们编程插件,使其在启用后向服务器广播一条消息。当我们在测试时启用插件时,这条消息将显示出来。为此,我们将重写onEnable方法。此方法在插件启用时执行。模仿以下代码以添加方法:
public class MyFirstBukkitPlugin extends JavaPlugin {
public void onEnable() {
}
}
你将看到另一个灯泡,它会要求你添加@Override注解。点击它以自动添加代码行。如果你没有被提示添加重写注解,那么你可能在该方法标题中拼写错误了。
我们现在有了你未来所有插件的基石。
创建和调用新方法
让我们创建一个新的方法,该方法将向服务器广播一条消息。以下图表标注了方法的各个部分,以防你不熟悉它们:

创建一个名为broadcastToServer的新方法。我们将将其放置在MyFirstBukkitPlugin类中的onEnable方法下。我们只想在MyFirstBukkitPlugin类内部调用此方法,因此访问修饰符将是private。如果你想从插件中的其他类调用此方法,你可以移除修饰符或将其更改为public。该方法将不返回任何内容,因此将具有void返回类型。最后,该方法将有一个名为msg的字符串参数。创建此第二个方法后,你的类将看起来像以下代码:
public class MyFirstBukkitPlugin extends JavaPlugin {
@Override
public void onEnable() {
}
private void broadcastToServer(String msg) {
}
}
提示
下载示例代码
你可以从你购买的所有 Packt Publishing 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
我们将在新方法的主体中编写代码以完成其任务。我们想要向服务器广播一条消息。我们可以调用插件的getServer方法。然而,为了方便,Bukkit类在静态上下文中包含了许多服务器方法。当你翻阅上一章中的Bukkit类 API 时,你可能已经遇到了这些方法;如果你没有遇到,请在hub.spigotmc.org/javadocs/spigot/index.html?org/bukkit/Bukkit.html中浏览Bukkit类的所有方法,直到找到broadcastMessage(String message)方法。我们将从自己的broadcastToServer方法中调用broadcastMessage方法。在 IDE 中,输入Bukkit,这是类的名称,以表示你将从静态上下文中访问Bukkit类。继续输入一个点(*)以调用该类的方法。你会看到将出现一个可用方法的列表,我们可以简单地滚动它们并选择我们想要的。这将在以下屏幕截图中显示:

点击选择broadcastMessage方法。该方法的 API 文档将会显示。注意,在方法右侧,它写着int。这告诉我们该方法返回一个整数类型的数据。当你点击查看更多链接,如前一张截图所示,文档会告诉我们返回的数字是消息发送到的玩家数量。我们并不关心这个数字。因此,我们不会将其分配给任何变量。
从列表中选择方法后,IDE 会填充它认为我们将使用的变量。在这种情况下,它应该使用msg作为参数。如果不是,只需在broadcastMessage 方法中输入msg作为参数即可。这完成了广播方法。现在,我们可以从onEnable方法中调用它。我们将传递Hello World!字符串作为参数。
添加这一行代码将导致包含以下代码的类:
public class MyFirstBukkitPlugin extends JavaPlugin {
@Override
public void onEnable() {
broadcastToServer("Hello World!");
}
/**
* Sends a message to everyone on the server
*
* @param msg the message to send
*/
private void broadcastToServer(String msg) {
Bukkit.broadcastMessage(msg);
}
}
如果我们测试这个插件,那么一旦启用,它将打印一次Hello World!。
扩展你的代码
在测试代码之前,让我们通过实现一个if语句来改进onEnable方法。如果只有一个玩家在线,为什么不向那个特定的玩家打招呼呢?我们可以通过调用Bukkit.getOnlinePlayers来获取所有在线玩家的集合。如果我们想检查集合的大小是否等于 1,我们可以通过使用if/else语句来完成。这在前面的代码中得到了演示:
if (Bukkit.getOnlinePlayers().size() == 1) {//Only 1 player online
//Say 'Hello' to the specific player
} else {
//Say 'Hello' to the Minecraft World
broadcastToServer("Hello World!");
}
在if语句中,我们现在将获取玩家集合中的第一个也是唯一的一个对象。一旦我们得到它,我们就可以通过广播Hello以及玩家的名字来继续操作。完成if语句后,整个类将看起来像以下代码:
package com.codisimus.myfirstbukkitplugin;
importorg.bukkit.Bukkit;
importorg.bukkit.entity.Player;
importorg.bukkit.plugin.java.JavaPlugin;
/**
* Broadcasts a hello message to the server
*/
public class MyFirstBukkitPlugin extends JavaPlugin {
@Override
public void onEnable() {
if (Bukkit.getOnlinePlayers().size() == 1) {
//Loop through the collection to access the single player
for (Player player : Bukkit.getOnlinePlayers()) {
//Say 'Hello' to the specific player
broadcastToServer("Hello " + player.getName());
}
} else {
//Say 'Hello' to the Minecraft World
broadcastToServer("Hello World!");
}
}
/**
* Sends a message to everyone on the server
*
* @param msg the message to send
*/
private void broadcastToServer(String msg) {
Bukkit.broadcastMessage(msg);
}
}
小贴士
如果您不完全理解if语句或提供的代码,那么我建议您访问我的网站学习 Java 的基础知识,这是本书前言中提到的先决条件。
摘要
您的第一个插件已经完成,并准备好在您的服务器上进行测试。在下一章中,我们将安装您的新插件,学习如何测试它,并了解服务器何时执行onEnable方法。现在您已经熟悉了编写和调用方法,您现在可以创建基本的插件。从现在开始,您创建的每个插件都将以类似于这个插件启动的方式开始,也就是说,首先创建一个新的项目,将 Bukkit 作为库添加,然后填写plugin.yml文件,最后将您的主体类设置为具有onEnable方法的JavaPlugin类。
第四章。在 Spigot 服务器上测试
Bukkit 插件设计为在 CraftBukkit 或 Spigot 服务器上运行。到目前为止,您有一个 Spigot 服务器和一个简单的插件。完成本章后,您的新插件将安装到您的服务器上。在本章中,您将对插件代码进行修改,并且您将很快看到这些更改在您的服务器上得到反映。这将帮助您更快地开发插件,并允许您在创建新插件时完成更多任务。您还将学习如何调试代码,以便在它不正常工作时进行修复。本章将涵盖以下主题:
-
为您的插件构建 JAR 文件
-
在您的服务器上安装插件
-
测试插件
-
测试插件的最新版本
-
调试代码
构建 JAR 文件
为了在服务器上安装插件,我们需要一个 .jar 文件。JAR 文件是一个 Java 可执行文件。它包含所有编写的代码,这些代码打包在一个 ZIP 文件格式中。这些代码需要被翻译,以便计算机能够理解和运行它。
在 NetBeans 中,有一个单独的按钮,我们可以点击它来构建项目。这将生成我们需要的 .jar 文件。让我们在我们的项目中添加一段代码,以便自动将创建的 .jar 文件复制到更方便的位置。在 NetBeans 中,点击 文件 选项卡以访问项目的 build.xml 文件,如下截图所示:

打开 build.xml 并在 import file 行之后添加以下代码块:
<target name="-post-jar">
<copy file="${dist.jar}" todir="../Plugin Jars" failonerror="true"/>
</target>
这段额外的代码将在 JAR 文件成功构建后执行。它将从 dist 目录将 JAR 文件复制到指定的位置。您可以将 "../Plugin Jars" 更改为您想要的任何目录。在这里,.. 表示向上移动一个文件夹。因此,如果您的 NetBeans 项目位于 C:\Users\Owner\Documents\NetBeansProjects\MyFirstBukkitPlugin,那么 .jar 文件将被复制到 C\Users\Owner\Documents\NetBeansProjects\Plugin Jars\MyFirstBukkitPlugin.jar。将此代码添加到每个插件中,将使它们在一个中央文件夹中保持组织。添加此新代码后,您的文件将类似于以下代码片段:
<?xml version="1.0" encoding="UTF-8"?>
<project name="MyFirstBukkitPlugin" default="default" basedir=".">
<description>Builds, tests, and runs the project MyFirstBukkitPlugin.</description>
<import file="nbproject/build-impl.xml"/>
<target name="-post-jar">
<copy file="${dist.jar}" todir="../Plugin Jars" failonerror="true"/>
</target>
</project>
小贴士
在前面的代码中,failonerror 被设置为 true。这意味着如果 JAR 文件未能复制到指定的位置,构建时将显示错误。这种错误可能表明位置不存在或您权限不足。如果您不想看到这些警告,可以将此值设置为 false。
注意,您将在<!--和-->之间有许多额外的行。这些都是注释,我鼓励您阅读它们,如果您想了解更多关于您可以在build.xml文件中添加的内容。一旦保存此文件,您就准备好构建项目了。您可以通过点击锤子图标或使用F11快捷键来完成。位于工具栏上的锤子图标看起来如下所示:

如果 NetBeans 无法成功构建 jar 文件,那么您可能代码中存在错误。
这些错误最有可能通过红色线条和灯泡显示出来,正如在第三章创建您的第一个 Bukkit 插件中看到的那样。您通常可以通过悬停或点击灯泡来获取帮助。如果您无法这样做,请参考上一章检查您的代码是否正确。如果您仍有疑问,请参考第二章学习 Bukkit API以获取帮助。
安装插件
新插件的安装相当简单。您需要从您之前在服务器plugins文件夹中选择的目录创建.jar文件的副本。然后,像往常一样启动您的服务器。您将看到控制台输出通知您插件已加载,如下面的截图所示:

如果您的服务器启动时没有看到Hello World!消息,请不要担心。这种行为是正常的,因为在这个时候,永远不会在线有玩家来接收广播的消息。目前,我们只关心上一张截图中突出显示的消息。
每次您对代码进行更改时,您都必须构建一个新的 JAR 文件并安装新版本。要安装新版本,您只需将其复制并粘贴到服务器的plugin文件夹中,并覆盖旧文件。这通常可以在不关闭服务器的情况下完成。然而,如果服务器正在运行,您将需要使用reload命令来加载新版本。
如果您不希望每次在代码中做出更改时都手动复制plugin .jar文件并将其粘贴到服务器的插件文件夹中,那么您可以在build.xml中自动化它。除了复制jar文件并将其粘贴到Plugin Jars目录外,您还可以直接将其复制并粘贴到服务器的plugins目录中。为此,添加一个第二个copy file标签,并将todir设置为您的服务器plugin目录。以下代码是示例:
<?xml version="1.0" encoding="UTF-8"?>
<project name="MyFirstBukkitPlugin" default="default" basedir=".">
<description>Builds, tests, and runs the project MyFirstBukkitPlugin.</description>
<import file="nbproject/build-impl.xml"/>
<target name="-post-jar">
<copy file="${dist.jar}" todir="../ Plugin Jars" failonerror="true"/>
<copy file="${dist.jar}" todir="C:/Users/Owner/Desktop/Bukkit Server/plugins" failonerror="true"/>
</target>
</project>
再次强调,您应该为每个您想在服务器上自动安装的插件都这样做。
测试您的插件
如您所忆,第一个插件的目的在于发现插件何时被加载。通过在控制台中输入以下命令来发出reload命令:
>reload
您将看到 Spigot 会自动禁用并重新启用插件,如下面的截图所示:

这次,当您的插件被启用时,您将看到Hello World!消息。如果正好有一个玩家在线,那么它会向该玩家打招呼。让我们通过登录服务器并在游戏中发出重新加载命令来观察这一点。打开您的 Minecraft 客户端并连接到您的服务器。从游戏中,首先发出以下命令:
/plugins
您将看到一个已安装插件的列表。目前,只有一个插件,如下面的截图所示:

现在服务器上有一个玩家,我们可以通过重新加载服务器来测试插件。在游戏中发出以下命令:
/reload
注意,在游戏和控制台中,您都会看到Hello Codisimus消息,如下面的截图所示,以表明插件按预期工作:

测试插件的新版本
插件按预期工作,但总有改进的空间。让我们通过向其中添加代码来继续对这个插件进行工作。
当消息为白色时,玩家可能看不到hello消息。我们可以使用ChatColor Enum来更改消息的颜色。这个Enum constants包含了游戏中支持的所有颜色代码,这样我们就可以轻松地将它们添加到消息中。让我们修改插件并在服务器上安装新修改的版本。选择您喜欢的颜色并将其放在broadcastToServer方法中的消息之前,如下面的代码所示:
Bukkit.broadcastMessage(ChatColor.BLUE + msg);
在构建新的 JAR 文件之前,将plugin.yml中的版本更改为0.2以表示这是一个更新版本。每次对代码进行修订时,您都会创建一个新的版本。将版本号更改为反映代码更改将确保新代码将分配一个唯一的版本号。如果您需要知道特定版本的项目中包含的代码更改,这将非常有价值。
使用构建图标或F11键构建一个新的 JAR 文件。如果您没有设置build.xml来自动执行此操作,请将新版本复制并粘贴到plugins文件夹中。再次发出reload命令以查看结果,如下面的截图所示:

插件已被重新加载,消息现在已着色。此外,注意当插件被禁用时版本号的变化,以及当它被加载并启用时的变化。这清楚地表明插件的新版本已成功安装在服务器上。
尝试自己进一步扩展这个插件以测试不同的代码。以下列表包含了一些供您挑战的内容:
-
将插件编程为显示世界的实际名称而不是单词World。这个挑战的一个提示是你可以获取所有世界的列表,然后使用列表中的第一个世界。请注意,这将广播
Hello world!,除非你在server.properties中重命名了世界。 -
向玩家发送消息,而不是向整个服务器广播消息。
-
如果在线玩家超过一个,向每个玩家发送独特的问候消息。这个提示是你可以使用一个
for循环。 -
如果没有玩家在线,为每个世界发送独特的问候消息。
调试代码
当你开发这个插件以及其他 Bukkit 插件时,你编写的一些代码可能不会按预期工作。这是由于代码中某个地方存在的错误造成的。这些错误被称为bug,找到这些 bug 并移除它们的过程称为调试。
从错误中学习
当你的插件第一次尝试不工作时,不要气馁。即使是经验丰富的程序员也会在他们的代码中遇到 bug。你的软件不完美并不意味着你是一个糟糕的开发者。能够发现 bug 并修复它们是软件开发的重要组成部分。你犯的错误越多,你能从中学到的就越多。以下是一个明显的例子。
有朝一日,你可能会编写一个包含玩家列表的插件。然后你可以编写以下for循环来遍历每个玩家并移除那些处于CREATIVE模式的玩家:
for (Player player : playerList) {
if (player.getGameMode() == GameMode.CREATIVE) {
playerList.remove(player);
}
}
当你测试这段代码时,很可能会发生错误。抛出的错误将是一个ConcurrentModificationException方法。异常的名称可能对你来说意义不大,但它将帮助你缩小问题范围。
小贴士
开发者不需要知道如何修复每个错误,但他们应该知道在哪里找到有关这些错误的信息,以便他们可以找出如何修复它们。这通常可以在软件的文档或公开的消息板上找到。大多数开发者会记下错误信息,并使用 Google 搜索相关信息。搜索结果通常会是官方文档或遇到相同问题的其他人的帖子。
要了解更多关于错误的信息,你可以搜索ConcurrentModificationException;你可能会找到来自 Oracle 的Javadoc中的以下声明:
"例如,通常不允许一个线程在另一个线程迭代它时修改一个集合。"
官方的 javadoc 可能很有用。但有时,它们仍然难以理解。幸运的是,存在像stackoverflow.com这样的网站,允许程序员互相帮助调试代码。如果你查看搜索结果,你会看到指向 Stack Overflow 问题和类似网站的帖子。这些链接非常有帮助,因为通常有像你一样的人遇到了相同的错误。如果你查看其他人的问题和提供的答案,你可以了解为什么会出现这个错误以及如何修复它。
在阅读了与并发修改错误相关的问题后,你最终会了解到,在大多数情况下,异常发生在你尝试在遍历列表或集合时对其进行修改时。你还会发现,为了避免这种情况,你必须使用迭代器。通常,会有如何修复错误的示例。在这种情况下,有关于如何正确使用迭代器从列表中删除对象的解释。如果不存在解释,那么你可以像使用 Bukkit 的 javadoc 一样,在 Oracle 的 javadoc 中研究迭代器。我们可以通过使用迭代器来修复之前的代码,如下所示:
Iterator<Player> itr = playerList.iterator();
while (itr.hasNext()) {
Player palyer = itr.next();
if (player.getGameMode() == GameMode.CREATIVE) {
itr.remove();
}
}
在修复代码中存在的并发修改错误后,你现在已经成为了一名更有经验的程序员。你将知道如何在未来避免这个问题,甚至在过程中学习了如何使用迭代器。
当研究不足以解决问题时
有时候,查阅文档和阅读论坛消息不足以修复错误。如NullPointerException这样的错误非常常见,可能由多种原因引起。通过研究,你会发现NullPointerException发生在你尝试访问一个null对象的某个方法或字段时。“null”指的是没有值。因此,null 对象是一个不存在的对象。然而,知道这一点并不总是能帮助你找到确切的哪个对象具有 null 值以及它最初为什么是 null 值。为了帮助找到错误,以下是一些可以遵循的步骤来定位有问题的代码。
阅读堆栈跟踪
Java 中的大多数错误都以堆栈跟踪的形式呈现。堆栈跟踪会告诉你错误发生之前正在执行的代码行。在 Spigot 服务器上,这些堆栈跟踪将类似于以下截图:

小贴士
如果你的服务器托管在其他地方,而你通过在线浏览器工具查看控制台,堆栈跟踪可能会以相反的顺序打印出来。
无论何时你的服务器出现异常,Spigot 都会记录错误以及导致错误的插件。有时,甚至会有错误发生时正在进行的特定事件的详细信息。通过前一个屏幕截图中的堆栈跟踪,我们可以看到错误是由MyFirstBukkitPlugin 版本 0.3引起的。如果版本与你在IDE中拥有的版本不匹配,你将想要用插件的最新版本更新服务器。这样,你可以确保服务器上运行的代码与你在 NetBeans 中拥有的代码相同。我们还可以看到异常是在插件被启用时抛出的。在下一行,我们看到具体的错误,即NullPointerException。在随后的那一行,我们被告知导致错误的精确代码行。它发生在MyFirstBukkitPlugin类的onEnable方法中。括号中写着MyFirstBukkitPlugin.java:27。这告诉我们错误发生在MyFirstBukkitPlugin类的第 27 行,当然是在onEnable方法中。堆栈跟踪的前三行对我们最有用。你很少需要查看更后面的行来解释。有时,你甚至看不到堆栈跟踪开头代码中的任何类名。然而,如果你继续查看,你可能会看到熟悉的类和方法名。
现在你已经知道了类名和行号,你可以回过头来看看你的代码,看看你是否注意到为什么你会得到NullPointerException。在 NetBeans 中,我可以看到第 27 行的代码如下:
Bukkit.getPlayer("Codisimus").sendMessage("Hello, there are " + Bukkit.getOnlinePlayers().size() + " player(s) online and " + Bukkit.getWorlds().size() + " world(s) loaded.");
拆分代码
麻烦的行是一行非常长的代码。因此,无法明显看出哪个对象具有空值。如果你发现自己处于类似的情况,我建议你将代码拆分成多行。这将给我们以下代码:
Player player = Bukkit.getPlayer("Codisimus");
int playerCount = Bukkit.getOnlinePlayers().size();
int worldCount = Bukkit.getWorlds().size();
player.sendMessage("Hello, there is " + playerCount + " player(s) online and " + worldCount + " world(s) loaded.");
在安装并运行这段新代码后,你应该在控制台中看到相同的错误,但它会指向不同的代码行。使用这段新代码,你现在会看到异常是在第 30 行抛出的,这是前一段代码的最后一行。
添加调试信息
在那单行代码中仍然有很多事情在进行。因此,你可能不确定哪个变量是空的。是player、playerCount还是worldCount?如果你需要一些额外的帮助,你可以在代码中添加我们所说的调试信息。这些信息可以将信息打印到控制台日志中,以指示代码中的发生情况。在 Bukkit 插件中记录信息有几种方法。最简单的方法是使用System.out.println(String string)方法。然而,更好的做法是利用 Spigot 服务器分配给你的插件的记录器。记录器可以通过getLogger方法获取。这个方法在JavaPlugin类中。你可以在onEnable方法中访问它。这些调试信息将是临时的。因此,你可以使用你喜欢的任何方法。但我确实建议你尝试使用记录器,因为它也会打印出插件信息。在我们的例子中,我们将使用记录器。
现在我们知道了如何打印消息,让我们记录每个变量的值,如下所示:
Player player = Bukkit.getPlayer("Codisimus");int playerCount = Bukkit.getOnlinePlayers().size();int worldCount = Bukkit.getWorlds().size();Logger logger = getLogger();logger.warning("player: " + player);logger.warning("playerCount: " + playerCount);logger.warning("worldCount: " + worldCount);player.sendMessage("Hello, there is " + playerCount + " player(s) online and " + worldCount + " world(s) loaded.");
小贴士
注意,我们在有问题的代码行之前添加了调试信息。一旦抛出异常,计算机就会停止执行代码。因此,如果调试信息在sendMessage调用之后,这些信息将永远不会被打印。

一旦你安装并运行更新后的代码,你将在控制台中看到调试信息:
现在,我们可以清楚地看到player有一个null值。
回顾 Javadoc
如果你回顾一下设置player的代码行,然后阅读 Bukkit 的 javadoc,你会了解到player的值为 null,因为请求的玩家 Codisimus 不在线。如果玩家找不到,则返回null。

正如你所见,代码中的 bug 可能并不正好在堆栈跟踪给出的行。在这种情况下,空值是在几行之前设置的。
在理解了问题之后再修复 bug
现在 bug 已经被揭露,我们可以修复它。在NullPointerException的情况下,有两种解决方案。不要仅仅因为你可以而简单地以某种方式修复 bug。你应该努力理解 bug 存在的原因以及代码应该如何运行。也许,变量 player 永远不应该有 null 值。如果我知道玩家 Codisimus 总是在线,那么也许,我拼写用户名时犯了错。然而,我们知道 Codisimus 并不总是在线。所以,在这个插件中,玩家变量有时会有 null 值。在这种情况下,我们不想尝试向玩家发送消息,因为这会抛出NullPointerException。为了解决这个问题,我们可以在if语句中放置代码行,开发者通常称之为 null 检查:
Player player = Bukkit.getPlayer("Codisimus");
int playerCount = Bukkit.getOnlinePlayers().size();
int worldCount = Bukkit.getWorlds().size();
//Logger logger = getLogger();
//logger.warning("player: " + player);
//logger.warning("playerCount: " + playerCount);
//logger.warning("worldCount: " + worldCount);
if (player != null) {
player.sendMessage("Hello, there is " + playerCount + " player(s) online and " + worldCount + " world(s) loaded.");
}
小贴士
注意,我已经通过在前面加上 // 将调试信息改为注释,这样这些信息就不会打印到日志中。或者,如果我觉得我永远不会再需要这些行,我也可以完全删除这些行。
现在我们已经添加了空值检查,只有当玩家不为空时,消息才会被发送。
摘要
你现在知道如何从 NetBeans 项目创建 JAR 文件。对于你未来将创建的插件,你可以遵循这个简单的流程来安装和运行你的新插件,无论是用于测试还是用于成品。你也知道如何更新已安装在服务器上的插件,并修复代码中暴露的 bug。在接下来的章节中,我们将创建越来越复杂的插件。这一步骤的第一步是为玩家在游戏中执行命令的插件创建命令。
第五章。插件命令
Bukkit API 的好处是它已经在其框架中内置了基本功能。作为程序员,我们不需要费心将这些基本功能实现到插件中。在本章中,我们将讨论这些功能之一,即玩家可以执行的在游戏中的命令。这些命令与您已经熟悉的命令类似,例如/reload、/gamemode或/give。我们将创建一个插件来对物品进行附魔。在本章结束时,一旦插件完成,您将能够输入/enchant来为您手中的物品添加您喜欢的附魔。
命令是玩家与插件通信的最简单方式之一。它们还允许玩家触发插件代码的执行。出于这些原因,大多数插件都将有一些命令。Bukkit 开发团队意识到了这一点,并为我们提供了一个简单的方式来注册命令。通过 Bukkit 注册命令确保插件知道当玩家输入命令时。它还可以防止插件与其他插件的命令发生冲突。以下是我们将涵盖的三个步骤,以向插件添加命令:
-
通知 Bukkit 一个插件将使用一个命令
-
编程插件在有人输入命令时将执行的操作
-
将新编写的代码分配给特定的命令
在 plugin.yml 中添加一个命令
按照第三章创建您的第一个 Bukkit 插件中所述的方式创建一个新的 Bukkit 插件,命名为Enchanter。或者,您也可以复制现有的项目并修改名称、包等信息以创建一个新的插件。这将消除添加所需库和配置构建脚本的必要性。可以通过以下步骤复制项目:
-
右键单击您希望复制的项目,然后从菜单中选择复制…
-
设置项目名称。项目位置应保持不变。
-
打开第四章中讨论的
build.xml,并将项目的名称更改为步骤 2 中设置的名称。 -
通过右键单击包并选择重命名…在重构菜单项中,更新您的新项目中的包,以确保它是唯一的。
-
如有必要,重命名主类。您还可以删除那些您知道不会再次使用的函数或类。
-
最后,使用新的插件信息修改
plugin.yml文件,包括名称、主类、版本和描述。
接下来,我们将通过修改插件的plugin.yml文件来通知 Bukkit 我们将使用一个命令。如第二章中所述的学习 Bukkit API,Spigot 读取 YAML 文件以找出有关插件所需的所有信息。这些信息包括您的插件将处理的全部命令。每个命令都可以有一个描述、正确的使用消息和别名,这与rl是reload的别名类似。我们将用于插件的命令将是enchant。通常,使用小写字母来表示命令,这样玩家在输入游戏命令时不必担心大小写。以下是在添加enchant命令后plugin.yml将如何显示的示例代码:
name: Enchanter
version: 0.1
main: com.codisimus.enchanter.Enchanter
description: Used to quickly put enchantments on an item
commands:
enchant:
aliases: [e]
description: Adds enchantments to the item in your hand
usage: Hold the item you wish to enchant and type /enchant
注意这些行的缩进。这种缩进必须是空格,而不是制表符。NetBeans 在您键入时自动缩进必要的行。此外,即使您使用了Tab键,NetBeans 也会自动使用空格。在 YAML 文件中,缩进非常重要,因为它决定了键的层次结构。enchant command在commands下缩进,表示它是插件的命令。aliases、description和usage命令在enchant下缩进,表示它们属于enchant命令。
小贴士
这三个设置的顺序无关紧要,它们是可选的。
如果发生错误或玩家错误地使用命令,将显示使用消息。可以通过发出插件的帮助命令来查看描述消息,即/help Enchanter。
对于aliases,我们有一个e作为值。这意味着如果我们觉得/enchant太长难以输入,我们可以输入/e。您可能有更多的别名,但它们必须以 YAML 列表格式放置。YAML 文件中的列表可以以两种不同的方式创建。第一种格式涉及通过逗号和空格分隔每个项目,并将整个列表括在方括号中,如下面的代码片段所示:
aliases: [e, addenchants, powerup]
第二种格式涉及将每个项目放在新的一行上,该行以连字符和一个空格开始,如下面的代码片段所示:
aliases:
- e
- addenchant
- powerup
通常,列表的长度决定了首选的方法。第二种格式在列表很长时更容易阅读。然而,请注意,在连字符之前不要有额外的或缺失的空格,因为这会在程序尝试读取列表时引起问题。一般来说,确保您的列表对齐。有关 YAML 语言的更多信息,请访问www.yaml.org/spec/1.2/spec.html。
可以轻松地将多个命令添加到插件中。以下代码是plugin.yml文件中包含多个命令的示例:
name: Enchanter
version: 0.1
main: com.codisimus.enchanter.Enchanter
description: Used to quickly put enchantments on an item
commands:
enchant:
aliases: [e, addenchants]
description: Adds enchantments to the item in your hand
usage: Hold the item you wish to enchant and type /enchant
superenchant:
aliases:
- powerup
disenchant:
description: Removes enchantments from the item in your hand
usage: Hold the item you wish to disenchant and type /disenchant
编程命令动作
一旦您将命令添加到plugin.yml文件中,您就可以开始编写命令将触发的代码。在 NetBeans 项目中创建一个新类。这个新类将被称为EnchantCommand。如果您愿意,可以给这个类起其他名字,但请记住,类的名字应该在不打开它的情况下就能让你了解这个类是如何使用的。将这个类放在与Enchanter(主插件类)相同的包中,如下面的截图所示:

小贴士
请记住,尽管包的结构相似,但您将使用自己的唯一命名空间,而不是com.codisimus
这个新类将执行enchant命令。因此,它必须实现CommandExecutor接口。我们将向类头添加代码来实现这一点。这类似于向Enchanter类添加extends JavaPlugin。JavaPlugin是一个类。因此,我们用我们的类扩展了它。CommandExecutor是一个接口,这意味着我们必须实现它。一旦我们在EnchantCommand类头中添加了implements CommandExecutor,就会有一个灯泡出现,提示我们需要导入CommandExecutor类。导入该类,灯泡仍然存在。现在它正在通知我们,因为我们实现了接口,我们必须实现其所有抽象方法。点击灯泡来实现,所需的方法就会出现。这个新方法将在玩家执行enchant命令时被调用。该方法提供了以下四个参数:
-
CommandSender sender-
默认情况下,此命令可以命名为
cs,但我们将命名为sender,因为它容易忘记cs代表什么 -
这是发送命令的人
-
它可能是一个玩家、控制台、命令方块,甚至是另一个插件创建的定制
CommandSender接口
-
-
命令 cmnd-
这是发送者执行的
命令对象 -
我们将不需要这个,因为这个类将仅用于单个命令
-
-
字符串别名-
这是发送者输入的别名
-
例如,它可能是
enchant、e、addenchant或powerup
-
-
String[] args-
这是一个字符串数组
-
每个字符串都是发送者输入的参数
-
参数跟在别名后面,并由空格分隔
-
命令本身不被视为参数
-
例如,如果他们输入
/enchant knockback 5,那么knockback将是第一个参数(args[0]),而5将是第二个也是最后一个参数(args[1]) -
在这一点上,我们不需要担心参数,因为
enchant命令不需要任何参数
-
如前所述,有不同类型的CommandSenders。以下是为CommandSender的继承图:

在这个图中,你可以看到Player、ConsoleCommandSender以及几个其他类都是CommandSender的子类型。增强命令的目的允许玩家增强他们持有的物品。因此,非玩家CommandSender对象将不会使用这个命令。在onCommand方法中,我们首先写的代码是检查是否有玩家执行了命令。如果我们不执行这个检查,那么当非玩家尝试发出enchant命令时,插件将会崩溃。我们将通过使用if语句和instanceof关键字来检查这一点。相应的代码如下:
if (sender instanceof Player)
这段代码可以翻译为以下内容:
如果命令发送者是玩家
这个if语句将告诉我们是否是玩家发送了命令。如果命令发送者不是玩家,那么我们希望停止执行代码。我们将通过使用return关键字来完成这个操作。然而,这个方法的return类型是boolean。我们必须返回一个boolean值,这将告诉 Bukkit 是否需要向命令发送者显示使用信息。通常,对于onCommand方法,如果你希望命令没有成功执行,你想要返回false。在这种情况下,它并没有。因此,我们将使用return false;代码。到目前为止,在方法内部,我们已经构建了以下代码:
if (sender instanceof Player) {
return false;
}
然而,这并不完全正确。这要求 Bukkit 在命令发送者是玩家时返回false,但当我们想要返回false时,情况正好相反。我们可以通过添加一个感叹号来实现这一点。如果你还不了解,在 Java 中,感叹号是一个NOT运算符,可以用来反转boolean值。我们将通过反转结果值来纠正之前的代码,如下面的代码所示:
if (!(sender instanceof Player)) {
return false;
}
注意额外的括号。这非常重要。括号允许表达式被分组。我们想要反转由sender instanceof Player代码产生的boolean值。如果没有括号,我们就会尝试反转发送者对象,这是没有意义的。因此,代码将无法编译。
到目前为止,EnchantComand类代码如下:
package com.codisimus.enchanter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/**
* Enchants the item that the command sender is holding
*/
public class EnchantCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command cmnd,String alias, String[] args) {
//This command can only be executed by Players
if (!(sender instanceof Player)) {
return false;
}
}
}
现在我们已经处理了非玩家,我们确定CommandSender对象是一个玩家。我们希望与Player对象而不是CommandSender对象一起工作,因为Player对象将手中有特定的物品。我们可以通过将CommandSender对象转换为Player来获取Player对象。通过转换,我们告诉 Java 我们知道命令发送者实际上是一个Player对象,而不是ConsoleCommandSender对象或其他子类型。转换是通过以下语法完成的:
Player player = (Player) sender;
小贴士
如果你还不熟悉转换,我再次建议你学习一些这些编程概念,请访问codisimus.com/learnjava。
现在我们有了Player 对象,我们需要他们所持有的物品。查看Bukkit API 文档中关于Player类的文档,该文档可在hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/Player.html找到,你可以看到存在一个getItemInHand方法,该方法是从HumanEntity继承而来的。它将返回ItemStack类,这正是我们想要的。这在上面的代码片段中得到了演示:
ItemStack hand = player.getItemInHand();
在对这件物品进行任何操作之前,我们必须确保确实有一个物品可以附魔。如果玩家在手中没有物品时运行命令,我们不希望插件崩溃。我们将检查ItemStack 类的值是否为null以及物品类型是否为AIR。在任何情况下,我们都会返回false,如下所示,因为命令没有执行:
if (hand == null || hand.getType() == Material.AIR) {
return false;
}
小贴士
如果我们在代码中不包含null检查(hand == null),我们可能会遇到Testing on the Spigot Server中讨论的NullPointerExceptions 错误。
现在,我们有了对玩家和他们所持物品的引用。我们的最终目标是附魔这个物品。再次查看 API 文档,我们可以在hub.spigotmc.org/javadocs/bukkit/org/bukkit/inventory/ItemStack.html找到添加到ItemStack 类的几个方法。阅读描述以找出哪个适合我们。
两个方法用于一次性添加多个附魔。我们可能想要添加多个附魔,但为了简化代码,我们将一次只添加一个。剩下的两个方法是addEnchantment(Enchantment ench, int level)和addUnsafeEnchantment(Enchantment ench, int level)。
小贴士
不安全方法的描述中指出:此方法不安全,将忽略等级限制或物品类型。请自行决定使用。 提供此警告是因为这些不安全的附魔尚未经过测试,可能会产生不理想的结果。你不应该让这个警告阻止你使用该方法,但在与朋友一起使用之前,你将想要测试附魔,以确保它不会使服务器崩溃。
因此,如果我们选择使用unsafe,我们可以创建强大的附魔,例如 10 级的锋利度。没有插件,剑的锋利度限制在 5 级。使用不安全的附魔,我们还可以附魔之前无法附魔的物品,例如带有KNOCKBACK或FIRE_ASPECT的鱼。现在,你将开始发现所有你可以用插件做的有趣和酷的事情,这些事情在原版游戏中是无法做到的。
从个人经验来看,我发现KNOCKBACK附魔相当有趣。在我的例子中,我将KNOCKBACK应用到物品上,但你当然可以选择你喜欢的任何附魔。关于附魔的完整列表及其功能,请访问 API 文档hub.spigotmc.org/javadocs/bukkit/org/bukkit/enchantments/Enchantment.html或 Minecraft 维基minecraft.gamepedia.com/Enchanting#Enchantments。Bukkit 警告我们,使用不安全的方法可能会引起问题。为了避免冲突,尽量将附魔等级保持在 10 级或以下。对于大多数附魔,在 10 级之后你甚至可能不会注意到任何区别。我们决定使用addUnsafeEnchantment(Enchantment ench, int level)。此方法接受一个Enchantment和一个int值作为参数。这个int值当然是附魔的等级,正如 API 文档中所述。我们已经决定了每个参数应该是什么。我们可以完成以下代码行,如下所示:
hand.addUnsafeEnchantment(Enchantment.KNOCKBACK, 10);
为了增加乐趣,我们还将添加FIRE_ASPECT附魔,如下面的代码片段所示:
hand.addUnsafeEnchantment(Enchantment.FIRE_ASPECT, 1);
到这一点,一切都将成功执行。在我们返回true之前,我们应该向玩家发送一条消息,让他们知道一切按计划进行。我们将使用sendMessage方法通过以下代码行只向这位玩家发送消息。服务器上的其他人,包括控制台,将看不到这条消息:
player.sendMessage("Your item has been enchanted!");
完成的类在以下代码行中显示。记住在编写代码时注释你的代码。以下代码中的某些注释可能看起来不必要,因为代码本身很容易阅读。我们将用术语自文档化来引用此代码。你只需要为可能在未来难以理解或需要澄清的代码留下注释。在你仍在学习的时候,我鼓励你过度使用注释。它们的存在不会造成任何伤害,并且在你需要时可以清楚地解释代码:
package com.codisimus.enchanter;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
/**
* Enchants the item that the command sender is holding
*/
public class EnchantCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command cmnd,String alias, String[] args) {
//This command can only be executed by Players
if (!(sender instanceof Player)) {
return false;
}
//Cast the command sender to a Player
Player player = (Player) sender;
//Retrieve the ItemStack that the Player is holding
ItemStack hand = player.getItemInHand();
//Return if the Player is not holding an Item
if (hand == null || hand.getType() == Material.AIR) {
return false;
}
//Add a level 10 Knockback enchantment
hand.addUnsafeEnchantment(Enchantment.KNOCKBACK, 10);
//Add a level 1 Fire Aspect enchantment
hand.addUnsafeEnchantment(Enchantment.FIRE_ASPECT, 1);
player.sendMessage("Your item has been enchanted!");
return true;
}
}
之前的代码实现了 enchant 命令。它验证命令发送者是一个玩家,并且该玩家正在持有物品。然后它将定义的附魔添加到物品上。这完成了在EnchantCommand类中需要完成的工作。
分配 enchant 命令的执行者
我们几乎准备好开始在服务器上使用这个命令了。唯一剩下的步骤是将我们刚刚编写的类分配给enchant命令。这通常被称为注册命令。在Enchanter类的onEnable方法中,我们将使用getCommand("enchant")代码获取enchant命令。
小贴士
命令的名称必须与plugin.yml中的完全一致。这也意味着这段代码只会检索特定于该插件的所有命令。
一旦我们有了附魔命令,我们就可以设置一个新的EnchantCommand实例作为该命令的执行者。所有这些都可以在一行中完成,如下面的代码片段所示:
getCommand("enchant").setExecutor(new EnchantCommand());
main类中您将拥有的所有内容如下所示:
package com.codisimus.enchanter;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Enchants the item that the command sender is holding
*/
public class Enchanter extends JavaPlugin {
@Override
public void onEnable() {
//Assign the executor of the enchant command
getCommand("enchant").setExecutor(new EnchantCommand());
}
}
摘要
现在,您有一个可以在自己的服务器上使用的有用插件。您可以像前一章讨论的那样构建这个插件,并将其放在您的服务器上进行测试。尝试使用不同的物品,并观察它是如何工作的。可以创建许多仅通过使用命令来运行的插件。有了这些知识,您有潜力创建大量的插件。您可以尝试一些插件,比如使用/spawn命令将您传送到世界的出生点的插件,一个使用/scare <player>命令向特定玩家播放 Creepers 嘶嘶声的插件,以及一个使用/strike <player>命令用闪电击中玩家的插件,您自己也可以尝试。
小贴士
World类中有一个名为strikeLightning的方法。
对于插件,您将需要使用参数。首先,您需要检查是否给出了正确的参数数量。然后,您需要获取第一个参数,正如本章前面所解释的那样。这个参数将是玩家的名称。Bukkit类中有一个方法可以用来根据给定的名称查找玩家。
如果您正在寻找插件灵感,请记住 API 文档是一个很好的灵感来源。此外,人们总是在 Bukkit、Spigot 和 Minecraft 论坛上寻找要制作的插件。在下一章中,我们将通过添加权限来扩展Enchanter插件。这将确保只有特权玩家才能使用enchant命令对物品进行附魔。
第六章:玩家权限
玩家权限是几乎每个 Bukkit 服务器管理员都希望在他们的服务器上拥有的一个功能。在原版的 Minecraft 中,你要么是OP(操作员),要么只是一个普通玩家。有了权限,你可以在两者之间创建无限数量的等级。有几个权限插件可以在 Bukkit 或 Spigot 网站上找到。在过去,开发者必须编写自己的代码来支持一个或多个这些权限系统。幸运的是,Bukkit API 现在为玩家权限提供了一个基础,这使得我们的工作变得容易。我们不再需要为每个存在的权限插件学习一个新的 API。我们只需要支持 Bukkit 的通用权限系统,我们可以确信它不会在任何时候发生剧烈变化。在本章中,你将做这件事,并安装一个权限插件,帮助你组织每个玩家的权限。到本章结束时,你将能够以确保不受信任的玩家不会破坏其他人的乐趣的方式来控制你的服务器。在本章中,我们将涵盖以下主题:
-
在你的服务器和插件中使用权限的好处
-
权限节点是什么以及开发者和服务器管理员如何使用它
-
将权限节点添加到
plugin.yml文件中 -
将权限节点分配给你的插件的一个命令
-
在游戏中测试玩家权限
-
安装和配置第三方权限插件
-
在你的插件中使用权限节点
权限的好处
权限让你对你的服务器上的玩家有更多的控制。它们允许你防止不受信任的玩家滥用。有了权限,你可以根据玩家在服务器中的角色和他们的可信度给予每个玩家一个特定的等级。比如说,你想要给某个特定的玩家赋予将其他玩家位置传送到自己位置的能力。有了权限,你可以这样做,而不必给那个玩家赋予生成物品、踢出/封禁其他玩家,甚至完全停止服务器的能力!一个有用的权限的最简单例子就是不给新玩家建造权限。这阻止了有人登录你的服务器,仅仅是为了破坏世界。他们将无法破坏你或其他玩家的建筑。
当编程插件时,你可以将某些权限分配给特定的命令或操作。这允许你只将插件的好处给予有特权的人。例如,你可能只想让你的好朋友和你自己有使用enchant命令对物品进行附魔的选项。完成这一步骤的第一步是了解权限节点是什么以及它们是如何工作的。
理解权限节点
权限节点是一个包含多个单词并由点号分隔的string。这些权限节点被赋予玩家,以在服务器上给予他们特殊的特权。一个例子是minecraft.command.give,这是执行give命令所需的权限节点。如您所见,它可以分解为三个部分,即创建者(Minecraft)、类别(命令)和特定特权(give命令)。您会发现大多数权限节点都是这样结构的。对于插件,其权限节点以插件的名称开头。这有助于防止节点冲突。如果两个插件使用相同的权限节点,那么管理员无法限制对一个节点的访问而不限制另一个节点。您还会发现许多插件的权限节点只有两个单词长。这是在插件没有很多权限时进行的。因此,不需要类别。另一方面,对于大型插件,您可能希望包含许多嵌套类别。
为了帮助您理解权限节点,我们将为Enchanter插件创建一个权限节点。权限节点的第一个单词将是插件的名称,而第二个单词将是命令的名称。如果权限节点直接与特定命令相关,那么在权限节点中使用命令名称是明智的。这将使您的权限易于理解和记忆。enchant命令的权限节点将是enchanter.enchant。如果我们预计这个插件有多个权限,那么我们可以使用enchanter.command.enchant。这两个权限节点都可以,但我们将使用前者作为示例。请注意,大多数开发者倾向于将权限节点保持为小写。这是可选的,但通常可以防止在稍后输入节点时出错。一旦我们确定了权限节点,我们必须将其添加到plugin.yml文件中,以便与插件一起使用。
将权限节点添加到 plugin.yml
在Enchanter项目中打开plugin.yml文件。添加权限节点的方式与添加命令类似。在新的一行中,添加permissions:。确保这一行没有任何缩进。在随后的行中,添加我们插件将使用的每个权限节点,后面跟一个冒号。接下来的几行将提供权限的属性,例如其描述。以下代码是添加了enchant权限节点后的plugin.yml文件示例。请确保缩进相似。请注意,版本属性也应更新,以表明这是一个新的、改进的Enchanter插件版本:
name: Enchanter
version: 0.2
main: com.codisimus.enchanter.Enchanter
description: Used to quickly put enchantments on an item
commands:
enchant:
aliases: e
description: Adds enchantments to the item in your hand
usage: Hold the item you wish to enchant and type /enchant
permissions:
enchanter.enchant:
description: Needed to use the enchant command
default: op
默认属性可以设置为true、false、op或not op。这决定了谁将拥有这个权限;true表示每个人都将拥有这个权限,false表示没有人将拥有它,op表示只有操作员将拥有它,而not op表示除了操作员之外的所有人将拥有它。可以通过使用权限插件进一步修改拥有这个权限的人,这将在本章后面讨论。
就像命令一样,你可以给插件分配多个权限。有关plugin.yml文件的更多信息,请访问wiki.bukkit.org/Plugin_YAML。
将权限节点分配给插件命令
现在我们已经创建了权限节点,我们希望阻止没有enchanter.enchant节点的玩家使用enchant命令。这个过程很简单,因为它只需要在plugin.yml文件中添加几行。
对于enchant命令,我们将添加两个属性,即permission和permission-message。permission属性简单地表示执行命令所需的权限节点。permission-message属性是一个消息,如果玩家没有必要的权限,他们将看到这个消息。在这些添加之后,plugin.yml文件将看起来像这样:
name: Enchanter
version: 0.2
main: com.codisimus.enchanter.Enchanter
description: Used to quickly put enchantments on an item
commands:
enchant:
aliases: [e]
description: Adds enchantments to the item in your hand
usage: Hold the item you wish to enchant and type /enchant
permission: enchanter.enchant
permission-message: You do not have permission to enchant items
permissions:
enchanter.enchant:
description: Needed to use the enchant command
default: op
您可能想给权限消息添加颜色。这可以通过使用§符号来完成。这是 Minecraft 用来指示颜色代码的字符。通过按住Alt键同时按下2然后1可以轻松地输入这个符号。所有颜色及其对应代码的列表可以在www.minecraftwiki.net/wiki/Formatting_codes找到。带有颜色支持的permissions-message行的示例如下:
permission-message: §4You do not have permission to §6enchant items

测试玩家权限
您可以通过构建jar文件并在您的服务器上安装它来测试插件的新增功能,如第四章所述,在 Spigot 服务器上测试。确保您重新加载或重启服务器,以便使用插件的新版本。请记住,当插件启用时,版本号会在控制台上打印出来。
通过在您的服务器上进行测试,您会发现您可以通过插件来附魔物品。由于您是 OP,您默认拥有enchanter.enchant节点。通过以下控制台命令来取消 OP自己:
>deop Codisimus
现在,您将无法再使用/enchant命令。
使用第三方权限插件
你很可能会在服务器上有一些值得信赖的玩家,你希望与他们分享/enchant命令的使用。然而,这些玩家还没有足够信任到可以成为 OP。为了共享这个命令的使用,你需要使用权限插件。权限插件将允许你创建多个玩家组。每个组将分配不同的权限。然后,每个在服务器上玩游戏的玩家都可以被分配到特定的组。例如,你可以有四个权限组,即default、trusted、mod和admin。default组将拥有基本权限。新加入服务器的玩家将被放入default组。trusted组将拥有更多一些的特权。他们将能够访问特定的命令,例如设置服务器世界的白天时间以及传送玩家。mod组代表“管理员”,它将能够访问许多其他命令,例如踢出或禁止玩家。最后,admin组代表“管理员”,它将拥有/give命令和/enchant命令。
在dev.bukkit.org上可以找到几个权限插件。每个权限插件都是由不同的开发者创建的。它们具有各种功能,这取决于开发者如何编程。今天使用的许多流行权限插件实际上是在权限添加到 API 之前创建的。因此,它们可能无法利用 Bukkit 的所有功能。它们还包括一些不再需要的附加功能,例如权限组。我们将使用的插件是我自己开发的,名为CodsPerms。CodsPerms是一个简单且基本的权限插件。因为CodsPerms遵循 Bukkit API 的规则,所以在本章中你将学习的组配置也可以用于其他权限插件。有关如何下载CodsPerms的说明可以在codisimus.com/codsperms找到。
一旦你有了插件的jar文件,就像安装你自己的插件一样在你的服务器上安装它。安装插件后,permission命令将可供你使用。执行/perm命令将告诉你现在可供使用的各种命令。
小贴士
你需要拥有permissions.manage节点才能使用权限命令。在我们完全设置权限插件之前,你可以要么从控制台运行这些命令,要么给自己分配 OP 状态。
你会发现有一些命令可以用来给玩家分配权限节点以及移除它们。如果你只想添加单个节点,比如给自己分配permissions.manage节点,这将很有用,但你可能不希望为所有加入你服务器的玩家使用这些命令。为了解决这个问题,我们将配置之前提到的组。
这些组将被创建为一个包含几个其他子权限节点的权限节点。这将允许我们给一个玩家一个单独的组节点,然后他们将继承其所有子节点。我们可以在位于 root 目录(即你放置 spigot.jar 的同一个文件夹)中的 permissions.yml 文件内创建这些父节点。permissions.yml 文件是一个 YAML 文件,就像 plugin.yml。因此,你应该熟悉其格式。你可以使用文本编辑器编辑此文件。如果你希望使用 NetBeans,你可以通过导航到 文件 | 打开文件… 或通过将文件拖放到 NetBeans 窗口中来打开文件。
小贴士
错误地编辑 YAML 文件会导致它无法完全加载。你很可能会遇到的问题是在你的文档中有一个 制表符 而不是 空格。这会导致你的文件无法正确加载。
以下代码是创建之前指定的组后 permissions.yml 可能看起来的一个示例:
group.default:
description: New Players who may have joined for the first time
default: true
children:
minecraft.command.kill: true
minecraft.command.list: true
group.trusted:
description: Players who often play on the server
default: false
children:
group.default: true
minecraft.command.weather: true
minecraft.command.time: true
minecraft.command.teleport: true
group.mod:
description: Players who moderate the server
default: false
children:
group.trusted: true
minecraft.command.ban: true
minecraft.command.pardon: true
minecraft.command.kick: true
group.admin:
description: Players who administer on the server
default: false
children:
group.mod: true
minecraft.command.ban-ip: true
minecraft.command.pardon-ip: true
minecraft.command.gamerule: true
minecraft.command.give: true
minecraft.command.say: true
permissions.manage: true
enchanter.enchant: true
每个组都可以通过简单地将该组权限节点添加为其子节点之一来继承另一个组的权限节点。在这个例子中,admin 组继承了 mod 组的所有权限,mod 组继承了 trusted 组的所有权限,而 trusted 组继承了 default 组的所有权限。因此,admin 组也通过父级继承了 default 组的权限。在这个示例文件中,我们将 group.default 父节点设置为 true。这意味着服务器上的每个玩家都将自动拥有 group.default 权限节点。由于子节点,每个玩家也将拥有 minecraft.command.kill 和 minecraft.command.list。将权限添加到默认组将消除向每个加入你服务器的玩家分配权限的需要。
如你所见,之前的权限节点包括了某些 Minecraft 命令的权限以及 Enchanter 插件的权限。还有更多权限尚未列出。这些是一些常用的权限。Minecraft 和 Bukkit 命令的其余权限可以在 wiki.bukkit.org/CraftBukkit_commands 找到。
一旦你填充了权限 YAML 文件,你必须重新加载服务器才能使更改生效。现在,你可以将玩家分配到不同的组。使用以下命令并替换为你自己的用户名来将自己添加到受信任的组:
>perm give Codisimus group.trusted
你将在permissions.yml文件中的group.trusted定义权限。尝试将自己放入不同的组中,并使用/enchant命令以及其他各种命令。确保你不是 OP,因为这会给你所有权限,无论你在哪个组中。此外,请注意,你必须手动从组中移除自己。如果一个admin组的玩家被添加到trusted组,他们仍然会保留管理员权限,直到他们从管理员组中移除。
在你的插件中使用权限节点
在某些情况下,你可能想在代码中检查玩家是否有特定的权限。随着 Bukkit 中通用权限系统的添加,这非常简单,无论你使用的是哪个权限插件。查看 Bukkit API 文档,你会看到Player对象包含一个hasPermission方法,它返回一个布尔响应。该方法需要一个string值,即正在检查的权限节点。我们可以将此方法放在一个if语句中,如下面的代码所示:
if (player.hasPermission("enchanter.enchant")) {
//Add a level 10 Knockback enchantment
Enchantment enchant = Enchantment.KNOCKBACK;
hand.addUnsafeEnchantment(enchant, 10);
player.sendMessage("Your item has been enchanted!");
} else {
player.sendMessage("You do not have permission to enchant");
}
这段代码对于插件来说是不必要的,因为 Bukkit 可以自动处理命令的玩家权限。为了了解如何正确使用,让我们回到MyFirstBukkitPlugin并添加一个权限检查。以下代码是修改后的onEnable方法,它只会向具有必要权限的玩家说Hello:
@Override
public void onEnable() {
if (Bukkit.getOnlinePlayers().size() >= 1) {
for (Player player : Bukkit.getOnlinePlayers()) {
//Only say 'Hello' to each player that has permission
if (player.hasPermission("myfirstbukkitplugin.greeting")) {
player.sendMessage("Hello " + player.getName());
}
}
} else {
//Say 'Hello' to the Minecraft World
broadcastToServer("Hello World!");
}
}
记住,你还需要修改plugin.yml来将权限节点添加到你的插件中。
你还可以向只有具有特定权限节点的玩家广播消息。有关此内容的文档可以在hub.spigotmc.org/javadocs/spigot/org/bukkit/Bukkit.html#broadcast(java.lang.String,%20java.lang.String)找到。
尝试将一些权限节点添加到之前章节中创建的其他项目中。例如,将creeperhiss.scare权限节点添加到具有/scare <player>命令的插件中。作为一个额外的挑战,添加一个选项,允许玩家输入/scare all来吓唬服务器上的所有玩家。在这种情况下,你可以检查每个玩家的creeperhiss.hear权限节点。这样,只有那些玩家会听到声音。这是一个很好的例子,说明权限节点应该默认设置为not op。
摘要
现有的插件经过修改后,在权限插件的辅助下变得更加灵活。当你的服务器运行CodsPerms时,你可以为玩家设置多个组。你可以创建为特定玩家提供特权命令的插件,同时这些玩家将无法使用可能被滥用的命令。这种关于 Bukkit 权限的新知识将使你能够更好地控制你的插件和服务器。现在你已经学会了如何编程命令和权限,你就可以深入探索 Bukkit API 中更具挑战性和趣味性的部分了。在下一章中,你将学习如何通过使用 Bukkit 事件系统来自动化和定制你的服务器。
第七章. Bukkit 事件系统
到目前为止,您已经知道如何创建一个在执行命令时运行一些代码的插件。这在许多情况下非常有用。然而,有时我们并不想输入命令。我们更希望代码能够自动触发执行。触发器可以是服务器上发生的一个特定事件,例如方块被破坏、爬行者爆炸,或者玩家在聊天中发送消息。Bukkit 事件系统允许开发者监听事件,并根据该事件自动运行一段代码。通过使用 Bukkit 事件系统,您可以自动化服务器,这意味着您在将来维护服务器时的工作量会减少。在本章中,我们将涵盖以下主题:
-
选择一个事件
-
注册事件监听器
-
监听事件
-
取消事件
-
事件之间的通信
-
在事件发生时修改事件
-
在自己身上创建更多插件
选择一个事件
Bukkit 提供的所有事件都可以在org.bukkit.event包的 API 文档中找到。每个事件都被分类到org.bukkit.event包内的各个包中,例如org.bukkit.event.block、org.bukkit.event.player和org.bukkit.event.world。这使得查找所需的事件变得容易。Bukkit 事件的完整列表可以在hub.spigotmc.org/javadocs/spigot/org/bukkit/event/class-use/Event.html找到。我鼓励您查看列表,看看您可以监听哪种类型的事件。每个事件都有几个方法,这些方法提供了更多信息,并允许您修改事件。例如,BlockBreakEvent提供了获取被破坏的方块和破坏它的玩家的方法。大多数事件也可以取消,如果您不希望事件发生。这在许多情况下都很有用,例如不让新玩家放置 TNT 方块,或者防止怪物生成。
如前所述,监听事件可以帮助自动化你的服务器并减少发送的命令数量。除此之外,它们还可以非常有乐趣去操作。让我们看看一些可以使用 Bukkit 事件系统制作的插件示例。我们提到你可以监听玩家聊天事件并随意修改它。你可以用它来监控消息并屏蔽可能被说出的冒犯性词语。放置 TNT 方块也被提到了。你可以创建一个插件,只有当玩家有build.tnt权限节点时才允许他们放置 TNT。还有一个可以取消的WeatherChangeEvent类。话虽如此,有许多服务器管理员不喜欢服务器下雨。雨声可能很大,很烦人。管理员会在每次雨开始时发出/toggledownfall命令来停止雨。在本章中,我们将创建一个插件,防止雨一开始就下起来。
我们必须做的第一件事是找到我们可以监听的正确事件。为了完成这个任务,我们将查看 Bukkit API 文档。假设我们对 API 不熟悉。因此,我们不确定可以使用哪个事件。我们可以查看事件列表,直到找到正确的一个,但如果你首先找到正确的包,你可能会有更好的运气。雨可能属于两个类别之一,即世界事件或天气事件。雨更有可能被归类为天气。所以,我们首先会查看那里。没有包含“雨”这个词的事件,因为雨被归类在雪里。因此,我们要找的事件是WeatherChangeEvent类。如果你没有找到正确的事件来使用,请查看其他包。
小贴士
如果你找不到你正在寻找的事件,那么请记住,你可以在 Bukkit/Spigot 论坛上寻求帮助。你也许可以先在论坛上搜索,看看是否有人正在寻找相同的信息。有可能你试图监听的事件根本不存在。请记住,Spigot 项目与 Minecraft 的创作者无关。因此,检测或修改某些事件是不可能的。
现在我们已经找到了事件,我们希望阻止这个事件的发生。查看WeatherChangeEvent类的参考页面,我们会看到在这个事件中提供了一些方法。我们将使用setCancelled方法来取消事件,并使用toWeatherState方法来确保我们只阻止雨开始,而不是停止。
注册事件监听器
在决定我们将监听哪个事件之后,是时候开始编程了。创建一个新的项目,如第三章中所述,创建你的第一个 Bukkit 插件,并将其命名为NoRain。别忘了创建一个plugin.yml文件。
为了监听一个事件,你的插件必须有一个注册为Listener class的类。在这个项目中,我们只有一个类,名为NoRain.java。因此,我们将这个类也作为Listener类。类的声明将类似于以下代码行:
public class NoRain extends JavaPlugin implements Listener
或者,如果这是一个大型项目,你可以为Listener class创建一个类,这类似于Enchanter项目有CommandExecutor作为单独的类。同样,像CommandExecutor一样,Listener class将实现一个interface method。我们希望实现的interface method是org.bukkit.event.Listener。
类被声明为Listener类,但它还没有在 Bukkit 中注册。为了在监听器中注册所有事件,请在onEnable方法中插入以下代码行:
getServer().getPluginManager().registerEvents(this, this);
这行代码检索PluginManager class并使用它来注册事件。PluginManager class用于多个目的,包括处理事件、启用/禁用插件和处理玩家权限。大多数时候,你会用它来注册事件监听器。它有一个registerEvents方法,该方法接受一个Listener对象和一个JavaPlugin对象作为参数。唯一存在的类既是Listener也是JavaPlugin。因此,我们将this对象传递给这两个参数。如果Listener类与main类分离,那么这行代码将类似于以下代码行:
getServer().getPluginManager().registerEvents(new WeatherListener(), this);
这就是onEnable方法中所需的所有内容。
监听事件
我们接下来要创建的方法是EventHandler method。我们将使用@EventHandler注解来告诉 Bukkit 哪些方法是事件监听器。创建一个新的方法,该方法只有一个我们选择的事件作为唯一参数:
public void onWeatherChange(WeatherChangeEvent event)
该方法必须是public的,并且不应该返回任何内容。你可以给这个方法取任何你想要的名称,但大多数程序员会保持名称与事件名称相似。
接下来,我们将指示这个方法处理事件。在方法上方添加以下注解:
@EventHandler
在同一行,我们可以修改EventHandler method的一些属性。你可能会添加到所有EventHandler方法中的一个属性是忽略已取消的事件。将ignoreCancelled属性设置为true将使方法看起来像这样:
@EventHandler (ignoreCancelled = true)
public void onWeatherChange(WeatherChangeEvent event) {
}
如果事件已经被其他插件取消,那么我们不想去麻烦监听它。
另一个属性是事件优先级。通过更改EventHandler 方法的优先级,你可以选择在或其他插件之前或之后监听事件。如果EventHandler 方法的优先级高于另一个事件,则它将在其他EventHandler 方法之后被调用,因此可能会覆盖第一个EventHandler 方法所做的任何修改。有六个优先级级别,它们的调用顺序如下:
-
LOWEST
-
LOW
-
NORMAL
-
HIGH
-
HIGHEST
-
MONITOR
因此,具有LOWEST优先级的插件首先被调用。想象一下,你有一个保护插件。你不想任何其他插件取消你的取消事件的决定。因此,你会将优先级设置为HIGHEST,这样就没有其他插件能够在你的插件之后修改事件。默认情况下,每个EventHandler 方法都有一个NORMAL优先级。如果你没有修改事件,那么你很可能会想在MONITOR级别监听。在修改事件(如取消它)时不应使用MONITOR优先级。
我们希望在具有NORMAL优先级的插件看到此事件之前取消此事件。因此,让我们将此事件的优先级更改为LOW。现在,方法上面的行看起来像以下代码行:
@EventHandler (ignoreCancelled = true, priority = EventPriority.LOW)
取消事件
最后,我们希望停止天气变化。为此,我们将调用事件的setCancelled方法。此方法接受一个Boolean值作为参数。我们希望canceled等于true。因此,我们将使用setCancelled(true)代码,如下所示:
package com.codisimus.norain;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.bukkit.plugin.java.JavaPlugin;
public class NoRain extends JavaPlugin implements Listener {
@Override
public void onEnable() {
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler (ignoreCancelled = true, priority =EventPriority.LOW)
public void onWeatherChange(WeatherChangeEvent event) {
event.setCancelled(true);
}
}
此插件将按原样工作。然而,还有改进的空间。如果服务器世界中已经下雨了怎么办?这个插件将阻止雨永远停止。让我们添加一个if语句,以便只有当天气开始时,WeatherChangeEvent 类才会被取消。事件提供了一个名为toWeatherState的方法,它返回一个Boolean值。此方法将返回true或false,告诉我们天气是开始还是停止。这也在 API 文档中得到了明确说明:

如果toWeatherState返回true,那么天气开始下雨。这是我们想要取消事件的情况。现在,让我们用 Java 写出同样的事情,如下所示:
if (event.toWeatherState()) {
event.setCancelled(true);
}
添加这个if语句后,你应该测试你的插件。在安装插件之前,登录到你的服务器并使用/toggledownfall命令来下雨。一旦下雨,安装你新创建的插件并重新加载服务器。此时,雨仍然在下,但你可以通过再次发出/toggledownfall命令来停止雨。如果你无法这样做,那么你添加的if语句可能是不正确的;复查它以找到你的错误并再次测试。一旦你停止了雨,你可以尝试使用相同的命令再次开始雨。只要代码正确,雨就不会开始。如果雨开始了,那么请验证你的事件监听器是否在onEnable方法中被正确注册。还要验证服务器是否启用了正确的插件版本,如第四章中所述,在 Spigot 服务器上测试。
事件之间的通信
插件完全按照预期工作,但如果我们改变主意,开始怀念雨声怎么办?或者,如果我们的城镇突然起火,必须迅速扑灭怎么办?我们不希望通过拒绝使用/toggledownfall命令来限制我们作为管理员的力量。接下来,我们将监听这个命令的发出,并在它被发出时允许天气变化。最终,我们仍然可以手动控制天气,但天气不会自行开始。
让我们再创建另一个EventHandler方法。这次,我们将监听控制台命令的发送,以便我们可以设置一个布尔标志,如下所示:
@EventHandler (ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
//Check if the Player is attempting to change the weather
if (event.getMessage().startsWith("/toggledownfall")) {
//Verify that the Player has permission to change the weather
if (event.getPlayer().hasPermission("minecraft.command.toggledownfall")) {
//Allow the Rain to start for this occasion
denyRain = false;
}
}
}
我们实际上不会对这次活动进行任何修改。因此,活动优先级将被设置为MONITOR。我们还想忽略已取消的事件。我们将监听的事件是PlayerCommandPreprocessEvent,这个事件会在玩家发出命令时发生,无论这些命令是针对 Minecraft、Bukkit 还是其他插件。我们只关心一个命令,即/toggledownfall。因此,第一个if语句检查消息是否以/toggledownfall开头。如果是一个不同的命令,我们将忽略它。正如事件名称所暗示的,这个事件在命令实际执行之前发生。因此,我们必须验证玩家是否有权运行该命令。该命令的权限节点是minecraft.command.toggledownfall。如果这两个条件都满足,那么我们希望在下一个WeatherChangeEvent类中允许雨开始。第二个EventHandler方法通过使用两个if语句并设置一个布尔变量为false来完成。
在这一点上,一个灯泡会出现,告诉你找不到 denyRain 符号。当你点击灯泡时,你可以选择 在 com.codisimus.norain.NoRain 中创建字段 denyRain。这将自动在类中创建一个名为 denyRain 的私有变量。注意新代码行的位置。它位于现有方法块之外,但仍在类内部。这很重要,因为它定义了变量的作用域。变量的作用域是它可以被访问的地方。denyRain 变量是私有的。因此,其他类,例如来自另一个插件的类,不能修改它。然而,在 NoRain 类内部,所有方法都可以访问它。这很有用,因为如果变量是在 onPlayerCommand 方法的花括号内声明的,我们就无法从 onWeatherChange 方法中看到它。
现在插件知道我们希望在何时允许雨开始,我们必须稍微修改 onWeatherChange 方法以允许这种例外。目前,要取消事件,我们将使用 true 作为参数调用 setCancelled 方法。如果我们传递 false 作为参数,那么事件将不会被取消。当我们要取消事件时,denyRain 变量等于 true。因此,而不是传递 true 或 false,我们可以传递 denyRain 的值。所以,当 denyRain 设置为 false 时,我们将使用以下代码行调用 setCancelled:
event.setCancelled(false);
在 onWeatherChange 方法的末尾,我们希望将 denyRain 的值重置为 true。这样,我们可以确保每次发出 /toggledownfall 命令时只允许天气改变一次。最终的代码如下:
package com.codisimus.norain;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.bukkit.plugin.java.JavaPlugin;
public class NoRain extends JavaPlugin implements Listener {
//This is a variable that our two methods will use to "communicate" with each other
private boolean denyRain = true;
@Override
public void onEnable() {
//Register all of the EventHandlers within this class
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler (ignoreCancelled = true, priority =EventPriority.LOW)
public void onWeatherChange(WeatherChangeEvent event) {
if (event.toWeatherState()) { //Rain is trying to turn on
//Cancel the event if denyRain is set to true
event.setCancelled(denyRain);
}
//Reset the denyRain value until next time a Player issues the /toggledownfall command
denyRain = true;
}
@EventHandler (ignoreCancelled = true, priority =EventPriority.MONITOR)
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
//Check if the Player is attempting to change the weather
if (event.getMessage().startsWith("/toggledownfall")) {
//Verify that the Player has permission to change the weather
if (event.getPlayer().hasPermission("minecraft.command.toggledownfall")) {
//Allow the Rain to start for this occasion
denyRain = false;
}
}
}
}
注意,当我们声明布尔型 denyRain 方法时,我们将其初始值设置为 true。
这就完成了 NoRain 插件。构建 JAR 文件并在你的服务器上测试它。使用这个新版本,你将能够使用 /toggledownfall 命令来停止和开始雨。
在事件发生时修改事件
Bukkit API 允许程序员做的不仅仅是取消一个事件。根据事件的不同,你可以修改其许多方面。在接下来的项目中,我们将修改僵尸在生成时的属性。每次僵尸生成时,我们将给它 40 点生命值,而不是默认的 20 点。这将使僵尸更难被杀死。
创建一个新的项目,就像为任何插件做的那样。我们将把这个插件命名为 MobEnhancer。类似于我们为 NoRain 插件所做的那样,让 main 类实现 Listener 并在 onEnable 方法中添加以下代码行以注册 EventHandlers 方法:
getServer().getPluginManager().registerEvents(this, this);
对于这个项目,我们将有一个EventHandler方法来监听怪物生成。这将是一个CreatureSpawnEvent类。这个事件有许多我们可以调用的方法,要么修改事件,要么获取更多关于它的信息。我们只想修改生成的僵尸。因此,我们将首先添加一个if语句,检查EntityType方法是否为ZOMBIE。这是通过以下代码块完成的:
if (event.getEntityType() == EntityType.ZOMBIE) {
}
在大括号内,我们将把Entity class的健康值改为40。我们可以通过调用event.getEntity()来检索Entity类。一旦我们有了Entity类,我们就能够访问许多额外的功能。你可以在 API 文档中查看所有这些方法,该文档可在hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/Entity.html找到。
其中一个方法是setHealth。在我们将健康值设置为40之前,我们必须设置最大健康值,其值可以是40。当实体的最大健康值仍然是20时,Entity class不能有40的健康值。这两行代码将完成此插件。现在的代码看起来是这样的:
package com.codisimus.mobenhancer;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.plugin.java.JavaPlugin;
public class MobEnhancer extends JavaPlugin implements Listener {
@Override
public void onEnable() {
//Register all of the EventHandlers within this class
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onMobSpawn(CreatureSpawnEvent event) {
if (event.getEntityType() == EntityType.ZOMBIE) {
int health = 40;
event.getEntity().setMaxHealth(health);
event.getEntity().setHealth(health);
}
}
}
MobEnhancer插件的第一个版本就完成了这个小类。你可以通过在你的服务器上安装它来测试插件。你会注意到僵尸将更难被杀死。
小贴士
注意,我们声明了一个名为health的局部变量,其类型为int,并将其值设置为40。或者,我们可以在接下来的两行中简单地写40。然而,以这种方式编程健康值允许我们轻松地在将来更改它。我们只需更改一行代码中的数字,而不是两行或更多。此外,你可能已经注意到setMaxHealth和setHealth方法接受double类型的变量。然而,仍然可以将int值传递给该方法,因为它将被自动转换为具有值40.0的double值。
你可以向插件中添加更多代码来修改更多类型实体的健康状态。所有EntityType方法的列表可以在 Bukkit API 文档中找到,位于EntityType类参考页面,具体网址为hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html。然而,在下一章中,我们将使此插件可配置,以便更改所有类型Entity的生存状态。
在你自己的插件中创建更多插件
现在你已经创建了这两个插件,你对如何正确使用事件监听器有了基本的了解。你现在有了创建数百个独特插件所需的知识。你开始所需的一切就是一个酷炫的想法。你为什么不尝试制作本章前面建议的插件之一呢?更多想法,你知道该去哪里找。Bukkit、Spigot 和 Minecraft 论坛或 API 文档都是寻找灵感的绝佳资源。例如,查看事件列表,我看到了ExplosionPrimeEvent类,它被描述为“当实体决定爆炸时调用”。当爬行者发出每个 Minecraft 玩家都害怕的嘶嘶声时,会调用此事件。当这种情况发生时,你可以向所有附近的玩家发送消息,让他们看起来像爬行者正在和他们说话。首先,你将为这个事件创建一个EventHandler 方法。如果你得到的实体不是爬行者,你将想要返回。然后,你将想要获取靠近爬行者的实体(Entity类中有一个这样的方法)。对于你得到的每个实体,如果它是一个玩家的实例,就发送给他们以下消息:
<Creeper> 那个东西真的很棒,你手里拿着的《ItemInHand》。如果它出了什么问题,那就太遗憾了。
在每条消息中,你将用玩家所持物品的类型替换<ItemInHand>。到这个时候,我相信你已经有了一些自己的想法,并且能够将它们实现出来。
你还应该了解关于监听器的一个好处,那就是如何注销它们。你可能永远不需要这样做,但如果你确实想停止修改或取消一个事件,那么你可以在Listener类中使用以下代码:
HandlerList.unregisterAll(this);
这将注销整个类。所以,如果你只想注销特定的EventHandler 方法,那么你应该将它们拆分到单独的类中。对于NoRain插件来说,注销监听器不是最佳选择,但如果你在添加/mobenhancer off命令时,它可能是有用的。然后,一个/mobenhancer on命令可以再次注册监听器,这与我们在onEnable方法中所做的方式类似。
摘要
我们在本章中制作的两个插件,其全部代码都在一个类中。然而,你可以选择将它们分开到主插件类和监听器类中。在这些小插件中,这并不是必要的。但在更大的项目中,这将使你的代码更加整洁。会有一些差异,比如使用静态变量或将变量传递给另一个类。在下一章中,我们将通过添加配置以及reload命令来完成MobEnhancer插件。我们将把Listener和CommandExecutor作为main类的一部分。一旦插件完成,我们将回顾同一插件作为三个独立类时的差异。
第八章. 使你的插件可配置
一个可配置的插件可以非常强大。一个插件将能够根据用户偏好以不同的方式运行。本质上,你的插件配置文件将类似于你的服务器的 bukkit.yml 文件。它将允许你更改插件设置而不修改 Java 代码。这意味着你不需要每次想要更改细节时都重新构建插件 JAR 文件。如果你的插件是公开的或被其他人使用,添加一个 config 文件可能会减少未来修改代码所需的时间。你的插件用户可以自己更改 config 文件中的设置,而无需开发者提供任何额外帮助。
为了完全理解我们为什么想要一个变量可配置,让我们看看我们之前讨论过的插件之一。在 MobEnhancer 中,我们将僵尸的生命值设置为 40 而不是 20。其他人可能希望使用你的插件,但他们希望将僵尸的生命值设置为 60。你可以创建两个版本的插件,这可能会变得非常混乱,或者你可以有一个可配置的版本。在你的服务器上的 config 文件中,僵尸的生命值将被设置为 40。但在另一个服务器上,生命值将被设置为 60。即使你的插件只在一个服务器上使用,配置也将允许快速轻松地更改生命值。
使你的插件可配置有五个步骤,如下所示:
-
决定你的插件哪些方面将是可配置的
-
创建一个包含每个设置及其默认值的
config.yml文件 -
添加代码以保存默认的
config文件以及加载/重新加载文件 -
读取配置的值并将它们存储在你的插件中的类变量中
-
确保你的代码引用了配置设置加载到的类变量
步骤不需要按此顺序执行,但我们将按以下顺序在本章中讨论:
-
可配置数据类型
-
编写
config.yml文件 -
保存、加载和重新加载你的插件配置
-
从配置中读取值
-
在你的插件中使用配置设置
-
在 YAML 格式中写入
ItemStack值 -
理解 YAML 结构和层次结构
-
本地存储配置值
-
将一个类拆分为多个类并从另一个类访问变量和方法
可配置数据类型
你可以轻松地将插件中的大多数变量设置为可配置。以下表格包含了各种数据类型及其可配置的原因示例:
| 数据类型 | 它如何被使用 |
|---|---|
int |
用于定义事件应发生的次数 |
double |
用于设置怪物出生时的生命值 |
boolean |
用于开启或关闭特定功能 |
String |
用于更改发送给玩家的消息 |
ItemStack |
使自定义物品出现 |
小贴士
将 ItemStack 值添加到配置文件中比较复杂,但这将在本章的末尾进行解释。
我们将使 MobEnhancer 可配置。我们希望玩家可以选择设置僵尸的生命值。这只是一个 double 类型的值。让我们扩展插件以支持额外的生物类型。我们首先创建 config 文件,然后调整程序以能够修改不同类型的怪物。因此,我们决定 config 文件将为每种怪物类型包含一个 double 数据类型的值。这个 double 值将是怪物的生命值。
编写 config.yml 文件
现在,是时候开始编写 config.yml 文件了。在 MobEnhancer 的默认包中创建一个新的 YAML 文件。为了使 Spigot 正确加载,此文件的名称必须是 config.yml。以下是一个 MobEnhancer 的配置文件示例。注意示例中由 # 字符表示的注释。请记住,始终包含注释,以便用户确切知道每个设置的用途:
#MobEnhancer Config
#Set the health of each Mob below
#1.0 is equal to half a heart so a Player has 20.0 health
#A value of -1.0 will disable modifying the mob's health
#Hostile
ZOMBIE: 20.0
SKELETON: 20.0
#Passive
COW: 10.0
PIG: 10.0
小贴士
在这个 config 文件中只包含了一些怪物,但所有怪物类型的名称都可以在 EntityType 类的 API 文档中找到,网址为 hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html。
这是一个简单的 YAML 文件,因为它不包含嵌套键。大多数配置都将像这样简单,但我们将在本章的后面讨论一些复杂的配置。
保存、加载和重新加载配置文件
现在我们有了 config.yml 文件,并且它位于插件的默认包中,我们需要能够将其保存到用户的服务器上。一旦文件保存,用户就可以随意编辑它。保存 config 文件就像在 onEnable 方法中添加以下方法调用一样简单,如下所示:
saveDefaultConfig();
这将把 config.yml 复制到 plugins/MobEnhancer/config.yml。如果文件已经存在,则此行代码将不会执行任何操作。
Spigot 会自动加载 config 文件,因此除了在需要实际访问配置文件时使用 getConfig 之外,你不需要在插件中做任何额外的事情。
重新加载 config.yml 相对简单,我们将以命令的形式添加,如下所示:
@Override
public boolean onCommand(CommandSender sender, Command command, String alias, String[] args) {
reloadConfig();
sender.sendMessage("MobEnhancer config has been reloaded");
return true; //The command was executed successfully
}
我们现在将此方法放在 main 类中。确保该类也实现了 CommandExecutor 接口。不要忘记使用以下行注册命令:
getCommand("mobenhancerreload").setExecutor(this);
命令也应该添加到 plugin.yml 中,就像往常一样。现在添加一个权限节点也是一个好主意。新的 plugin.yml 文件如下所示:
name: MobEnhancer
main: com.codisimus.mobenhancer.MobEnhancer
version: 0.2
description: Modifies Mobs as they spawn
commands:
mobenhancerreload:
description: Reloads the config.yml file of the plugin
aliases: [mereload, merl]
usage: /<command>
permission: mobenhancer.rl
permission-message: You do not have permission to do that
permissions:
mobenhancer.rl:
default: op
现在,你的插件将有一个reload命令。这意味着当你编辑config.yml时,你可以重新加载插件而不是重启整个服务器。
读取和存储配置的值
一旦加载了配置文件,你必须能够访问该文件并读取已设置的值。扩展主类的JavaPlugin类有一个getConfig方法,它返回FileConfiguration。这个FileConfiguration类就是我们用来获取我们正在寻找的值的。你会看到FileConfiguration类有getInt、getDouble、getString和getBoolean等方法;所有这些方法都接受一个字符串作为参数。string参数是值的路径。为了完全理解路径,我们需要查看一个包含嵌套键的 YAML 配置。一个例子是我们刚刚工作的plugin.yml文件。如果我们想从配置中获取MobEnhancer字符串,那么路径将是name。如果我们想检索mobenhancerreload命令的描述,那么路径将是commands.mobenhancerreload.description。因此,检索此值的 Java 代码将是getString("commands.mobenhancerreload.description");。MobEnhancer的config.yml文件相当简单。为了获取一个双精度值,我们可以使用getDouble()方法,路径为生物的名字。例如,要获取为ZOMBIE实体设置的值,我们将使用以下代码:
double health = this.getConfig().getDouble("ZOMBIE");
这将从以下三个来源之一返回一个double值:
-
从
plugins/MobEnhance/config.yml加载的FileConfiguration -
默认的
FileConfiguration,这是位于MobEnhancerJAR 文件默认包中的config.yml文件 -
数据类型的默认值(对于
double/integer数据类型为0,对于布尔值为false,对于字符串/ItemStack为null)
将返回第一个未失败的结果。一个结果会因路径无效或值无效而失败。在前面的声明中,如果ZOMBIE路径不在config.yml中,则会出现无效路径。无效值意味着给定路径的值不是double数据类型。
现在我们已经了解了如何读取配置的数据,让我们修改插件以使用这些自定义值。
在你的插件中使用配置设置
MobEnhancer插件的当前EventHandler方法将僵尸的生命值设置为40,其中数字 40 是硬编码的。这意味着40的值是代码本身的一部分,并且在代码编译后无法更改。我们希望将此值软编码,也就是说,我们希望从外部源获取该值,在我们的例子中是config.yml:
目前,onMobSpawn方法如下:
@EventHandler
public void onMobSpawn(CreatureSpawnEvent event) {
if (event.getEntityType() == EntityType.ZOMBIE) {
int health = 40;
event.getEntity().setMaxHealth(health);
event.getEntity().setHealth(health);
}
}
我们将从这个现有代码开始工作。不再需要if语句,因为我们不想将插件限制为仅适用于僵尸。如前所述,我们还想将硬编码的40 值替换为double值,该值将从config文件中读取。因此,40应替换为getConfig().getDouble(type)。您还必须将变量类型从int更改为double。此语句中的Type将是Entity类型的字符串。以下是一些示例,如ZOMBIE、SKELETON或config.yml中列出的任何其他实体类型。我们已经知道,我们可以使用event.getEntityType()获取由事件生成的实体的类型。然而,这给我们的是enum形式的EntityType,而我们需要的却是字符串形式。Bukkit API 文档的EntityType页面告诉我们,我们可以调用getName方法来返回我们想要的字符串。新的onMobSpawn方法如下:
@EventHandler
public void onMobSpawn(CreatureSpawnEvent event) {
//Find the type of the Entity that spawned
String type = event.getEntityType().name();
//Retrieve the custom health amount for the EntityType
//This will be 0 if the EntityType is not included in the config
double health = getConfig().getDouble(type);
event.getEntity().setMaxHealth(health);
event.getEntity().setHealth(health);
}
这个EventHandler 方法几乎完成了。我们允许其他人设置health值。我们想确保他们输入的是有效数字。我们不希望插件因为误用而崩溃。我们知道我们接收的是一个double值,因为即使用户设置了一个非数字值,我们也会得到默认值0。然而,并非每个有效的双精度值都适用于我们的情况。例如,我们不能将实体的健康值设置为负数。我们也不希望将健康值设置为0,因为这会立即杀死实体。因此,我们只应在新的健康值设置为正数时修改健康值。这可以通过一个简单的if语句来完成,如下所示:
if (health > 0)
MobEnhancer插件现在可配置并支持任何类型的生物。它不再仅限于僵尸。完成的代码将类似于以下内容:
package com.codisimus.mobenhancer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.plugin.java.JavaPlugin;
public class MobEnhancer extends JavaPlugin implements Listener, CommandExecutor {
@Override
public void onEnable() {
//Save the default config file if it does not already exist
saveDefaultConfig();
//Register all of the EventHandlers within this class
getServer().getPluginManager().registerEvents(this, this);
//Register this class as the Executor of the /merl command
getCommand("mobenhancerreload").setExecutor(this);
}
@EventHandler
public void onMobSpawn(CreatureSpawnEvent event) {
//Find the type of the Entity that spawned
String type = event.getEntityType().name();
//Retrieve the custom health amount for the EntityType
//This will be 0 if the EntityType is not in the config
double health = getConfig().getDouble(type);
//Mobs cannot have negative health
if (health > 0) {
event.getEntity().setMaxHealth(health);
event.getEntity().setHealth(health);
}
}
@Override
public boolean onCommand(CommandSender sender, Command command,String alias, String[] args) {
reloadConfig();
sender.sendMessage("MobEnhancer config has been reloaded");
return true; //The command was executed successfully
}
}
配置中的物品堆叠
接下来,我们将通过允许给僵尸和骷髅提供盔甲和武器的选项来进一步扩展MobEnhancer插件。为了做到这一点,我们首先必须学习如何在配置文件中将ItemStack对象作为选项添加。ItemStack方法比简单的整数或双精度值更复杂。它是一个具有许多嵌套值的对象。它也可能包括一个元数据值,该值将具有更多嵌套值。元数据包含关于物品的附加信息,例如自定义显示名称或构成物品附文的文本行。以下是一个YAML文件中的ItemStack方法示例:
SampleItem:
==: org.bukkit.inventory.ItemStack
type: DIAMOND_SWORD
damage: 1500
amount: 1
meta:
==: ItemMeta
meta-type: UNSPECIFIC
display-name: §6Sample Item
lore:
- First line of lore
- Second line of lore
- §1Color §2support
enchants:
DAMAGE_ALL: 2
KNOCKBACK: 7
FIRE_ASPECT: 1
一旦加载,结果中的项目将在以下屏幕截图显示:

只需要类型字段。你可以省略任何其他部分。类型指的是材料的类型。这些材料可以在 API 文档的org.bukkit.Material下找到,可以通过访问hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html来查看。损坏程度用于表示物品受到了多少损坏。对于像羊毛这样的物品,这将设置羊毛的颜色。数量将设置堆叠大小。例如,我可能有一把剑或二十根木头。元数据包括额外的信息,如旗帜的颜色和图案,或书籍的作者和页数。给定路径getConfig().getItemStack("SampleItem");将检索该物品。
YAML 配置层次结构
在使用 YAML 中的ItemStack时,请注意层次结构。这与plugin.yml文件中命令和权限的嵌套值类似。我们可以在config文件中使用层次结构,使其更容易使用和理解。
我们希望给两种类型的怪物分配物品,即Zombie和Skeleton。每种类型将有一个独特的盔甲和武器。这意味着我们需要 10 个不同的ItemStack类。我们可以将它们命名为ZombieHolding、SkeletonHolding、ZombieHelmet、SkeletonHelmet等等。然而,层次结构将更加高效。我们将有一个Zombie键和一个Skeleton键。在每个僵尸和骷髅内部,我们将为每个物品有一个键。以下是一个config文件中怪物盔甲段落的层次结构示例:
Zombie:
holding:
==: org.bukkit.inventory.ItemStack
type: STONE_SWORD
helmet:
==: org.bukkit.inventory.ItemStack
type: CHAINMAIL_HELMET
Skeleton:
holding:
==: org.bukkit.inventory.ItemStack
type: BOW
helmet:
==: org.bukkit.inventory.ItemStack
type: LEATHER_HELMET
小贴士
其余的盔甲部件可以以相同的方式添加。
如果我们想获取骷髅靴子的ItemStack方法,我们将使用getConfig().getItemStack("Skeleton.boots");。记住,层次结构是通过点来表示的。以下是一个将要附加到config.yml的段落,其中包含怪物盔甲,如前所述。我们还有一个GiveArmorToMobs布尔值,它将被包含以方便地禁用怪物盔甲功能:
### MOB ARMOR ###
GiveArmorToMobs: true
Zombie:
holding:
==: org.bukkit.inventory.ItemStack
type: STONE_SWORD
helmet:
==: org.bukkit.inventory.ItemStack
type: CHAINMAIL_HELMET
Skeleton:
holding:
==: org.bukkit.inventory.ItemStack
type: BOW
meta:
==: ItemMeta
meta-type: UNSPECIFIC
enchants:
ARROW_FIRE: 1
helmet:
==: org.bukkit.inventory.ItemStack
type: LEATHER_HELMET
color:
==: Color
RED: 102
BLUE: 51
GREEN: 127
将配置值存储为变量
从你的插件config文件中检索值比访问本地变量所需的时间和资源更多。因此,如果你将非常频繁地访问特定的值,最好将其存储为变量。我们希望用GiveArmorToMobs布尔值做这件事。将ItemStack盔甲本地存储也是一个好主意,以防止每次使用时都创建一个新的盔甲。让我们在主类的方法上方添加以下变量:
private boolean giveArmorToMobs;
private ItemStack zombieHolding;
private ItemStack skeletonHolding;
我们将只编写代码来设置僵尸或骷髅所持有的物品。其余的盔甲你可以自己添加,因为它们将以相同的方式进行。
我们希望这些值在config文件重新加载时自动存储。请注意,当config文件最初加载时,它实际上正在被重新加载。为了确保我们的数据在每次config文件重新加载时都得到保存,我们将在插件的reloadConfig方法中添加额外的代码。这是我们调用以执行/merl命令的方法。reloadConfig方法已经包含在每一个 Java 插件中,但我们将通过重写它来修改它。这就像我们重写onEnable方法一样。重写一个方法将阻止现有代码的执行。对于onEnable来说这不是问题,因为该方法没有先前的现有代码。然而,reloadConfig有我们仍然希望执行的代码。因此,我们将使用以下代码行来执行我们正在重写的现有代码:
super.reloadConfig();
这行代码非常重要。一旦我们有了它,我们就可以在它之前或之后添加自己的代码。在我们的例子中,我们希望在config文件重新加载后存储这些值。因此,额外的代码应该放在前面代码的后面。修改后的reloadConfig方法看起来是这样的:
/**
* Reloads the config from the config.yml file
* Loads values from the newly loaded config
* This method is automatically called when the plugin is enabled
*/
@Override
public void reloadConfig() {
//Reload the config as this method would normally do
super.reloadConfig();
//Load values from the config now that it has been reloaded
giveArmorToMobs = getConfig().getBoolean("GiveArmorToMobs");
zombieHolding = getConfig().getItemStack("Zombie.holding");
skeletonHolding = getConfig().getItemStack("Skeleton.holding");
}
我们必须编写的最后一行代码是为特定的怪物提供护甲。我们将把这个添加到onMobSpawn方法的末尾。我们只想在giveArmorToMobs设置为true时这样做。因此,代码块将被放置在一个if语句中,如下所示:
if (giveArmorToMobs) {
}
我们可以使用以下代码检索实体的护甲:
EntityEquipment equipment = event.getEntity().getEquipment();
这即使它们目前可能没有任何东西,也能给我们它们的装备槽位。要了解更多关于这个对象以及你可以用它做什么的信息,请访问其 API 文档hub.spigotmc.org/javadocs/spigot/org/bukkit/inventory/EntityEquipment.html。现在我们有了EntityEquipment,设置护甲件就简单了。
我们有两种不同的护甲集合。因此,我们首先需要检查实体是否是僵尸或骷髅。我们可以通过使用一个if/else语句来完成这个操作:
if (event.getEntityType() == EntityType.ZOMBIE) {
//TODO - Give Zombie armor
} else if (event.getEntityType() == EntityType.SKELETON) {
//TODO – Give Skeleton armor
}
然而,使用switch/case块将更有效率。在这种情况下使用switch/case将看起来像这样:
switch (event.getEntityType()) {
case ZOMBIE:
//TODO - Give Zombie armor
break;
case SKELETON:
//TODO - Give Skeleton armor
break;
default: //Any other EntityType
//Do nothing
break;
}
If/else语句用于检查多个条件(实体是僵尸吗? 或 实体是骨架吗?)。switch/case语句通过询问单个问题(以下哪个是实体的类型?)来节省时间。然后,将执行正确的case条件内的代码。当满足break条件时,switch语句将退出。如果你不在case结束时使用break,那么你将进入下一个case并开始执行那段代码。在某些情况下,这是好事,但我们不希望在这里发生这种情况。默认情况,即如果没有其他情况匹配,不需要包含它,因为没有代码在其中。然而,它确实使语句更加明确,并且 Oracle 发布的 Java 编码标准指出,默认情况应该始终包含。
在这些情况中的每一个,我们都希望装备正确的盔甲套装。
在应用以下代码之前,我们应该检查每一件盔甲以确保它不是null。这将防止插件由于无效配置而崩溃:
if (zombieHolding != null) {
equipment.setItemInHand(zombieHolding.clone());
}
小贴士
我们在这里使用了ItemStack上的clone方法。我们不想给每个怪物分发单个ItemStack类。相反,我们将创建它的副本,以便每个怪物都可以有自己的副本。
装备剩余的盔甲和将盔甲装备到骨架上非常相似。整体代码块将看起来像这样:
if (giveArmorToMobs) {
//Retrieve the equipment object of the Entity
EntityEquipment equipment = event.getEntity().getEquipment();
switch (event.getEntityType()) {
case ZOMBIE:
//Set each piece of equipment if they are not null
if (zombieHolding != null) {
equipment.setItemInHand(zombieHolding.clone());
}
//TODO – Add rest of armor
break;
case SKELETON:
//Set each piece of equipment if they are not null
if (skeletonHolding != null) {
equipment.setItemInHand(skeletonHolding.clone());
}
//TODO – Add rest of armor
break;
default: //Any other EntityType
//Do nothing
break;
}
}
应该在每个ItemStack类上调用clone方法,以确保原始物品不受损坏。
通过这种方式,MobEnhancer插件现在支持给怪物装备盔甲。在你的服务器上试用一下,看看它是如何工作的。我们只讨论了给僵尸和骨架装备盔甲,因为大多数怪物,包括爬行者、蜘蛛和牛,都不能穿盔甲。如果你想,尝试给其他怪物添加盔甲和物品,看看会发生什么。还可以尝试给怪物提供独特的物品。例如,骨架可以给一把剑,僵尸可以给一把弓。还有一个外观不同的头骨物品;你可以让怪物戴上它作为面具。
你甚至可以创建代表特定玩家(如 Notch)的头骨,如下面的截图所示:

NotchSkull物品的元数据如下:
NotchSkull:
==: org.bukkit.inventory.ItemStack
type: SKULL_ITEM
damage: 3
meta:
==: ItemMeta
meta-type: SKULL
skull-owner: Notch
在你的新插件上玩玩,看看你可以给僵尸和其他怪物提供什么疯狂物品。以下截图展示了通过修改配置可以实现的一个例子:

从另一个类访问变量
MobEnhancer 类正在不断增长。没有必要将所有代码都放在一个类中。这个类目前正扩展 JavaPlugin 类,同时实现 Listener 和 CommandExecutor 接口。如果我们将这些功能拆分为三个独特的类,程序将更容易理解。这个过程被称为 重构。在整个软件开发过程中,你可能会遇到可能过时或低效的代码,需要更新。以这种方式更改代码被称为 重构。如果你未来需要重构代码,不要气馁;这在软件开发中是常见现象,并且有许多原因会导致这种情况发生。
-
你学会了如何编写更高效的代码
-
API 变更或新功能需要/允许代码更改
-
现有的代码难以阅读或调试
-
一个方法/类变得太大,难以管理
-
代码的目的已经改变,现在它应该执行它最初并未打算执行的任务
我们将重构 MobEnhancer,将代码拆分为三个更易于管理的类。
创建两个新的类,分别命名为 MobSpawnListener 和 MobEnhancerReloadCommand。MobEnhancer 仍然是你的主类。因此,它仍然会扩展 JavaPlugin 类。然而,这两个新类将分别实现 Listener 和 CommandExecutor 接口。将适当的方法移动到它们的新类中,也就是说,onMobSpawn 是一个事件处理器,因此它属于 Listener 类,而 onCommand 属于 CommandExecutor 类。在移动方法时,你会看到引入了几个错误。这是因为你的方法不再能够访问必要的方法和变量。让我们首先解决 MobEnhancerReloadCommand 类,因为它只有一个错误。这个错误发生在以下行:
reloadConfig();
reloadConfig 方法位于 JavaPlugin 类中,它不再与 CommandExecutor 类合并。我们需要从这个独立的类中访问 JavaPlugin 对象。最简单的方法是使用静态变量。如果一个变量或方法是静态的,那么它不会在不同类的实例之间改变。这允许我们从静态上下文中引用变量。你之前在使用 Bukkit 类时已经这样做过了。你调用的方法是静态的。因此,你可以使用 Bukkit 类而不是一个独特的 Bukkit 对象来访问它们。
为了更好地解释这一点,让我们想象你有一个插件,它为 Minecraft 玩家提供银行账户。因此,你将有一个类来表示玩家的银行账户。这个类可以称为 PlayerAccount。你将有许多 PlayerAccount 对象,每个服务器上的玩家一个。在这个类中,你可能有一个变量定义了账户可以持有的金额上限。让我们称这个变量为 accountLimit。如果我们想每个账户的最大金额为 1000,那么 accountLimit 应该是静态的。如果我们想将限制提高到 2000,那么我们可以通过 PlayerAccount.accountLimit = 2000; 将 accountLimit 设置为 2000。然后,所有玩家现在都有 2000 的账户限制。如果我们想让一些玩家的限制为 1000,而其他玩家的限制为 2000,那么我们不应该使用静态变量。如果没有将 accountLimit 设置为静态,那么如果我们为 PlayerAccount 的实例 A 设置 accountLimit 为 2000,实例 B 的 accountLimit 仍然是 1000。
在主类中将插件存储为静态变量将对我们有益。在你的当前变量上方,添加一个名为 plugin 的 static JavaPlugin 变量,如下所示:
public class MobEnhancer extends JavaPlugin {
//Static plugin reference to allow access from other classes.
static JavaPlugin plugin;
我们还必须在 onEnable 方法中实例化这个变量。这可以通过 plugin = this; 简单完成。现在,我们可以通过使用 MobEnhancer.plugin 来访问插件实例。因此,我们之前有 reloadConfig();,现在我们将有 MobEnhancer.plugin.reloadConfig()。这将修复 MobEnhancerReloadCommand 中的错误:
package com.codisimus.mobenhancer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
public class MobEnhancerReloadCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String alias, String[] args) {
MobEnhancer.plugin.reloadConfig();
sender.sendMessage("MobEnhancer config has been reloaded");
return true; //The command executed successfully
}
}
MobSpawnListener 需要进行类似的修改,因为需要插件对象来调用 getConfig 方法。
你将继续在 MobSpawnListener 中看到错误。它正在尝试访问仍然在主类中的变量。让我们将怪物护甲变量移动到 Listener 类中,如下所示:
public class MobSpawnListener implements Listener {
private boolean giveArmorToMobs;
private ItemStack zombieHolding;
private ItemStack skeletonHolding;
我们还必须修改 MobEnhancer.java 中的 reload 方法,以匹配变量的新位置。例如,我们不应该使用 giveArmorToMobs,而应该使用 MobSpawnListener.giveArmorToMobs:
public void reloadConfig() {
//Reload the config as this method would normally do
super.reloadConfig();
//Load values from the config now that it has been reloaded
MobSpawnListener.giveArmorToMobs = getConfig().getBoolean("GiveArmorToMobs");
MobSpawnListener.zombieHolding = getConfig().getItemStack("Zombie. holding");
MobSpawnListener.skeletonHolding = getConfig(). getItemStack("Skeleton.holding");
}
即使有了这个更改,我们仍然会收到一个错误,错误信息为 giveArmorToMobs 在 MobSpawnListener 中具有私有访问权限。每个变量都是 private 的,这意味着它们不能从另一个类中访问。我们希望能够从其他类中访问它们。因此,我们将移除私有修饰符。这样做之后,我们还会收到另一个错误。这个新的错误信息为 非静态变量 giveArmorToMobs 不能从静态上下文中引用。这是因为变量没有被定义为静态变量。在你简单地将这些变量改为静态变量之前,请确保这样做是有意义的。参考之前关于何时应该使用静态变量的讨论。在这种情况下,我们只想为每个变量设置一个值。因此,我们确实希望将它们设置为静态,如下面的代码所示:
public class MobSpawnListener implements Listener {
static boolean giveArmorToMobs;
static ItemStack zombieHolding;
static ItemStack skeletonHolding;
剩下的代码中只有两行需要我们注意。这两行用于注册事件监听器和命令执行器。在调用registerEvents方法时,需要两个参数。第一个参数是Listener,第二个参数是Plugin。this关键字引用插件。因此,作为第二个参数是合适的。然而,对于第一个参数,你必须传递Listener类的实例。我们在第七章中这样做过,即创建NoRain插件时,Bukkit 事件系统。同样适用于命令执行器。我们必须传递MobEnhancerReloadCommand类的实例:
//Register all of the EventHandlers
getServer().getPluginManager().registerEvents(new MobSpawnListener(), this);
//Register the Executor of the /mobenhancerreload command
getCommand("mobenhancerreload").setExecutor(new MobEnhancerReloadCommand());
这消除了由于将项目拆分为多个类而产生的所有错误。
摘要
你现在已经熟悉了如何使用YAML配置文件。你可以从config.yml文件中加载自定义值并在插件中使用它们。这样做将大大扩展你创建对多个服务器管理员有益的独特项目的能力。尝试将可配置选项添加到你的某些先前项目中。例如,如果你创建了一个当爬行者即将爆炸时发送消息的插件,添加一个配置文件来设置玩家必须位于其中的区域,以便看到消息。现在你已经了解了可以与 Bukkit API 一起使用的FileConfiguration,在下一章中,我们将使用相同的FileConfiguration方法保存插件的数据,以便我们可以在插件下次启用时加载它。
第九章:保存你的数据
Bukkit 插件有很多种类型。其中一些需要你保存数据。通过保存数据,我指的是将信息保存到系统的硬盘上。如果信息必须在服务器重启后保持完整,则需要这样做。到目前为止,我们创建的插件中还没有这个要求。以下是一些将保存数据的插件示例:
-
经济插件必须保存每个玩家有多少钱的信息
-
土地保护插件必须保存有关哪些地块已被声明及其所有者的信息
-
任务插件必须存储每个任务的所有信息,例如谁完成了它
在服务器关闭时,保存数据有无数用途。在本章中,我们将创建一个传送插件,将各种传送位置保存到文件中。同样,我们将这些位置保存到文件中,这样在服务器关闭后我们就不需要再次创建它们。你已经熟悉 YAML 文件格式。因此,我们将利用 YAML 配置来保存和加载数据。在本章中,我们将涵盖以下主题:
-
你可以保存的数据类型
-
值得保存的插件数据和保存频率
-
扩展预先编写的传送插件
-
创建和使用
ConfigurationSerializable对象 -
在 YAML 配置中保存数据
-
从 YAML 配置文件中加载已保存的数据
可以保存的数据类型
你可能还记得,在上一章中讨论过,只有某些数据类型可以存储在 YAML 文件中。这些包括原始类型,如 int 和 boolean,字符串、列表以及实现 ConfigurationSerializable 的类型,如 ItemStack。
因此,我们只能保存这些特定类型的数据。
你可能希望保存其他类型的数据,例如 Player 对象,或者在传送插件的情况下,一个 Location 对象。这些可能不能直接存储,但通常可以分解以保存在以后加载时所需的重要值。例如,你不能保存 Player 对象,但你可以保存玩家的 UUID(通用唯一标识符),它可以被转换成字符串。每个 Player 都有一个 UUID。因此,这是我们后来能够引用该特定玩家的唯一信息。
小贴士
仅存储玩家名称不是一个充分的解决方案,因为提供的 Minecraft 账户名称可以更改。
Location 对象也不能直接存储,但它可以被分解为其世界、x、y 和 z 坐标、yaw 和 pitch。x、y、z、yaw 和 pitch 的值仅仅是可以存储的数字。至于世界,它也有一个永远不会改变的 UUID。因此,一个位置被分解为一个字符串(world uuid)、三个双精度浮点数(x、y、z)和两个浮点数(yaw 和 pitch)。
当你创建自己的插件时,你可能希望将某些类存储在文件中,例如一个 BankAccount 对象。如前所述,我们可以使用任何实现了 ConfigurationSerializable 的类来做到这一点。ConfigurationSerializable 意味着对象可以被转换成可以存储在配置中的形式。然后,这个配置可以被写入文件。在传送插件中,我们将创建一个 location 对象,它正好可以做到这一点。
保存哪些数据以及何时保存
我们知道可以保存到文件中的内容,但我们应该保存什么?将数据写入文件会占用磁盘空间。因此,我们只想保存我们需要的内容。最好是思考,“服务器关闭后,我想保留哪些信息?”例如,一个银行插件将希望保留每个账户的余额。作为另一个例子,一个 PvP 场地插件可能不会关心有关场地比赛的信息。在服务器关闭时,比赛很可能简单地被取消。在考虑传送插件时,我们希望服务器关闭后仍然保留每个传送门的位置。
我们接下来的关注点是何时保存这些信息。如果数据量很大,将数据写入文件可能会使服务器 延迟。如果你不熟悉“延迟”这个术语,它是一个用来表示服务器运行缓慢的短语。你知道这种情况发生时,因为游戏变得非常卡顿,玩家和怪物似乎四处移动。这对每个人来说都是一种不愉快的体验。因此,你只想在你必须的时候保存数据。保存数据频率有三个典型的选项:
-
每次数据被修改时
-
定期,例如每小时一次
-
当服务器/插件关闭时
这些选项按照它们的安全性排序。例如,如果你的数据仅在服务器关闭时保存,那么如果服务器崩溃,你将面临丢失未保存数据的风险。如果数据每小时保存一次,那么在最坏的情况下,你将丢失一小时的资料。因此,在可能的情况下,应始终使用第一个选项。第二个和第三个选项只有在插件处理大量数据且/或数据修改非常频繁时(例如每分钟修改几次)才应考虑。传送插件的数据只有在有人创建/删除传送门或设置他们的家传送门位置时才会被修改。因此,每次数据被修改时,我们将调用 save 方法。
一个示例传送插件
对于这个项目,你将得到一个不完整的传送插件。你已经知道如何编写这个项目的大部分代码。因此,我们只讨论以下三个主题:
-
创建一个实现
ConfigurationSerializable的类 -
save方法 -
load方法
插件的其余部分已提供,可以从www.packtpub.com下载,如前言中所述。你将工作的代码是插件 Warper 的 0.1 版本。通过插件并阅读注释,尝试理解它所做的一切。在这个项目中使用了Maps和try/catch块。如果你不知道它们是什么,那没关系。它们将在你需要使用它们的时候进行解释。请注意,SerializableLocation类是位置类,实现了ConfigurationSerializable;我们将在下面讨论这一点。
编写 ConfigurationSerializable 类
序列化是将数据或对象转换为可以写入文件的形式的过程。在 Warper 插件中,我们需要保存 Bukkit 位置信息。位置信息本身不能进行序列化。因此,我们将创建自己的类来保存 Bukkit Location对象的数据,并且能够将其转换为地图形式,以及从地图形式转换回来。如果你对地图还不熟悉,它们是一种非常有用的集合类型,我们将在整个项目中使用。地图有键和值。每个键指向一个特定的值。Warper插件是地图如何使用的良好示例。当进行传送时,玩家将通过名称选择一个特定的位置进行传送。如果所有传送位置都在一个列表中,我们就必须遍历列表,直到找到具有正确名称的传送位置。使用地图,我们可以将键(传送的名称)传递给地图,它将返回值(传送位置)。
创建一个名为SerializableLocation的新类,其中包含一个私有变量,用于保存 Bukkit Location 对象。第一个构造函数将需要一个Location对象。我们还将包括一个getLocation方法。以下是新类开头的外观代码:
package com.codisimus.warper;
import org.bukkit.Location;
/**
* A SerializableLocation represents a Bukkit Location object
* This class is configuration serializable so that it may be
* stored using Bukkit's configuration API
*/
public class SerializableLocation {
private Location loc;
public SerializableLocation(Location loc) {
this.loc = loc;
}
/**
* Returns the Location object in its full form
*
* @return The location of this object
*/
public Location getLocation() {
return loc;
}
}
一旦你添加了implements ConfigurationSerializable,你的 IDE 应该会警告你关于实现所有抽象方法。你必须重写的方法是serialize。这个方法将返回你对象的地图表示形式。我们之前已经提到了我们需要的数据。现在,我们只需要给每份数据分配一个名称并将其放入地图中。要向地图添加数据,你可以调用put方法。这个方法需要两个参数,即键和键的值。键是数据的名称,它允许我们稍后引用它。值是可序列化的数据。要了解更多关于地图的信息,你可以阅读docs.oracle.com/javase/8/docs/api/java/util/Map.html上的Javadoc。对于serialize方法,我们需要获取我们之前提到的所有数据并将其放入地图中,如下所示:
/**
* Returns a map representation of this object for use of serialization
*
* @return This location as a map of Strings to Objects
*/
@Override
public Map<String, Object> serialize() {
Map map = new TreeMap();
map.put("world", loc.getWorld().getUID().toString());
map.put("x", loc.getX());
map.put("y", loc.getY());
map.put("z", loc.getZ());
map.put("yaw", loc.getYaw());
map.put("pitch", loc.getPitch());
return map;
}
这处理了保存部分,但我们仍然需要处理加载部分。最简单的方法是添加一个接受地图作为参数的构造函数,如下所示:
/**
* This constructor is used by Bukkit to create this object
*
* @param map The map which matches the return value of the serialize() method
*/
public SerializableLocation(Map<String, Object> map) {
//Check if the world for this location is loaded
UUID uuid = UUID.fromString((String) map.get("world"));
World world = Bukkit.getWorld(uuid);
if (world != null) {
//Each coordinate we cast to it's original type
double x = (double) map.get("x");
double y = (double) map.get("y");
double z = (double) map.get("z");
//Both yaw and pitch are loaded as type Double and then converted to float
float yaw = ((Double) map.get("yaw")).floatValue();
float pitch = ((Double) map.get("pitch")).floatValue();
loc = new Location(world, x, y, z, yaw, pitch);
} else {
Warper.plugin.getLogger().severe("Invalid location, most likely due to missing world");
}
}
加载基本上是保存的反面。我们从映射中提取每个值,然后使用它来创建 Bukkit 的Location对象。作为一个安全措施,我们首先会验证世界是否实际上已经加载。如果世界没有加载,位置将不存在。我们不希望插件因为这一点而崩溃。也没有必要尝试加载一个不存在世界的位置,因为无论如何没有人能够传送到那里。
从映射中获取的每个对象都必须转换为它的原始类型,这在之前的代码中已经完成。float值是一个特例。每个float值都将被读取为double值。double值类似于float,但更精确。因此,将float值作为double值加载,然后转换它们不会导致数据丢失。
这两种方法都将由 Bukkit 使用。作为一个程序员,您只需将此对象存储在 YAML 配置中即可。这可以通过简单地使用以下代码行来完成:
config.set("location", serializableLoc);
然后,您可以使用以下代码检索数据:
SerializableLocation loc = (SerializableLocation)config.get("location");
Bukkit 使用serialize方法和构造函数来处理其余部分。
类名和路径用于引用这个类。为了查看一个例子,请查看MobEnhancer插件的config.yml文件中的ItemStack对象。这个类的例子也已经提供:
==: com.codisimus.warper.SerializableLocation
提示
路径当然会有您自己的命名空间,而不是com.codisimus。
这工作得很好,但可能会引起混淆,尤其是当路径名很长时。然而,有一种方法可以要求 Bukkit 通过别名引用这个类。执行以下两个步骤来完成:
-
在类上方直接添加
@SerializableAs注解,如下所示:@SerializableAs("WarperLocation") public class SerializableLocation implements ConfigurationSerializable { -
在
ConfigurationSerialization 类中注册您的班级:ConfigurationSerialization.registerClass(SerializableLocation.class, "WarperLocation");
这可以在onEnable方法中完成。只需确保它在尝试加载数据之前执行。
提示
可序列化的名称必须是唯一的。因此,最好包含您的插件名称,而不仅仅是Location。这样,您可以为另一个插件提供一个可序列化的位置,而不会发生冲突。
将数据保存到 YAML 配置文件
现在,我们准备完成save方法。我们希望将数据保存到 YAML 文件中,就像我们在config.yml中做的那样。然而,我们不想将其保存到config.yml中,因为它有不同的用途。我们首先需要做的是创建一个新的 YAML 配置,如下所示:
YamlConfiguration config = new YamlConfiguration();
接下来,我们将存储我们希望保存的所有信息。这是通过将对象设置到特定的路径来完成的,如下所示:
config.set(String path, Object value);
在本章前面提到了value的可接受类型。在传送插件中,我们有地图,其中包含SerializableLocation方法。只要它们是字符串到ConfigurationSerializable对象的映射,就可以将地图添加到 YAML 配置中。哈希表在配置中的添加方式不同。你必须使用地图创建一个配置部分。
以下代码显示了我们将如何将传送数据添加到配置中:
config.createSection("homes", homes);
config.createSection("warps", warps);
一旦所有数据都存储完毕,剩下的就是将配置写入save文件。这是通过在config上调用save方法并传递我们希望使用的文件来完成的。调用插件上的getDataFolder方法将给我们一个目录,我们应该在其中存储所有插件数据。这也是config.yml所在的位置。我们可以使用这个目录来引用我们将要保存数据的文件,如下所示:
File file = new File(plugin.getDataFolder(), "warps.yml");
config.save(file);
我们将所有这些代码行放入一个try块中,以捕获可能发生的异常。如果你还不了解异常,它们是在出现某种错误或发生意外情况时抛出的。可以使用try/catch块来防止错误导致你的插件崩溃。在这种情况下,如果由于某种原因指定的文件无法写入,则会抛出异常。这种原因可能是用户权限不足或找不到文件位置。因此,带有try块的save方法如下:
/**
* Saves our HashMaps of warp locations so that they may be loaded
*/
private static void save() {
try {
//Create a new YAML configuration
YamlConfiguration config = new YamlConfiguration();
//Add each of our hashmaps to the config by creating sections
config.createSection("homes", homes);
config.createSection("warps", warps);
//Write the configuration to our save file
config.save(new File(plugin.getDataFolder(), "warps.yml"));
} catch (Exception saveFailed) {
plugin.getLogger().log(Level.SEVERE, "Save Failed!", saveFailed);
}
}
以下是一个使用 Warper 插件创建的示例warps.yml文件:
homes:
18d6a045-cd24-451b-8e2e-b3fe09df46d3:
==: WarperLocation
pitch: 6.1500483
world: 89fd34ff-2c01-4d47-91c4-fa5d1e9fdb81
x: -446.45572804715306
y: 64.0
yaw: 273.74963
z: 224.9827566893271
warps:
spawn:
==: WarperLocation
pitch: 9.450012
world: 89fd34ff-2c01-4d47-91c4-fa5d1e9fdb81
x: -162.47507312961542
y: 69.0
yaw: -1.8000238
z: 259.70096111857805
Jungle:
==: WarperLocation
pitch: 7.500037
world: 35dafe89-3451-4c27-a626-3464e3856428
x: -223.87850735096316
y: 74.0
yaw: 87.60001
z: 382.482006630207
frozen_lake:
==: WarperLocation
pitch: 16.200054
world: 53e7fab9-5f95-4e25-99d1-adce40d5447c
x: -339.3448071127722
y: 63.0
yaw: 332.84973
z: 257.9509874720554
从 YAML 配置加载数据
现在完成save方法后,我们准备编写load方法。你已经熟悉使用 Bukkit 配置 API 加载数据。我们现在要做的是类似于在上一章中讨论的从config.yml检索值。然而,我们必须首先手动使用以下代码加载配置,这将有所不同。我们只有在文件实际存在的情况下才这样做。因此,我们不希望在第一次使用插件时出现错误:
File file = new File(plugin.getDataFolder(), "warps.yml");
if (file.exists()) {
YamlConfiguration config = new YamlConfiguration();
config.load(file);
现在我们已经加载了 YAML 配置,我们可以从中获取值。数据已放置在两个独特的配置部分中。我们将遍历这两个部分中的每个键,以加载所有位置。要从部分获取特定对象,我们只需要调用get方法并将其转换为正确的对象。你可以在完成的load方法中看到这是如何完成的:
/**
* Loads warp names/locations from warps.yml
* 'warp' refers to both homes and public warps
*/
private static void load() {
try {
//Ensure that the file exists before attempting to load it
File file = new File(plugin.getDataFolder(), "warps.yml");
if (file.exists()) {
//Load the file as a YAML Configuration
YamlConfiguration config = new YamlConfiguration();
config.load(file);
//Get the homes section which is our saved hash map
//Each key is the uuid of the Player
//Each value is the location of their home
ConfigurationSection section = config.getConfigurationSection("homes");
for (String key: section.getKeys(false)) {
//Get the location for each key
SerializableLocation loc = (SerializableLocation)section.get(key);
//Only add the warp location if it is valid
if (loc.getLocation() != null) {
homes.put(key, loc);
}
}
//Get the warps section which is our saved hash map
//Each key is the name of the warp
//Each value is the warp location
section = config.getConfigurationSection("warps");
for (String key: section.getKeys(false)) {
//Get the location for each key
SerializableLocation loc = (SerializableLocation) section.get(key);
//Only add the warp location if it is valid
if (loc.getLocation() != null) {
warps.put(key, loc);
}
}
}
} catch (Exception loadFailed) {
plugin.getLogger().log(Level.SEVERE, "Load Failed!",loadFailed);
}
}
现在插件已经完成,你可以在服务器上对其进行测试。设置一个家位置以及一些传送位置,然后查看保存的文件。停止并重新启动服务器以验证插件确实加载了正确的数据。
摘要
本章中我们创建的插件在数据被修改时都会调用save方法。在下一章中,你将学习如何定期保存数据。如果你希望在服务器关闭时保存数据,只需从插件的main类的onDisable方法中调用save方法。你可以利用你的编程技能来扩展这个插件。你可以添加权限节点,只需将它们添加到plugin.yml文件中即可。你还可以添加一个config.yml文件来修改消息或可能需要设置的即将到来的扭曲延迟的时间。如果你想要集成监听器,你可以监听PlayerRespawnEvent事件。然后,你可以设置玩家的重生位置为他们的家。有无数种方法可以自定义这个插件以满足你的需求。许多传送插件使用扭曲延迟来防止玩家在战斗中传送离开。在下一章中,我们将通过添加使用 Bukkit 调度器的扭曲延迟来扩展这个项目。
第十章. Bukkit 调度器
Bukkit 调度器是一个非常强大的工具,学习如何使用它也很容易。它允许你创建重复的任务,例如保存数据。它还允许你延迟执行代码块的时间。Bukkit 调度器还可以用于异步计算长时间的任务。这意味着像将数据写入文件或下载文件到服务器这样的任务可以调度在单独的线程上运行,以防止主线程,从而防止游戏卡顿。在本章中,你将通过继续在 Warper 传送插件上工作,以及创建一个名为 AlwaysDay 的新插件来学习如何完成这些任务。这个新插件将通过反复将时间设置为中午来确保服务器始终是白天。本章将涵盖以下主题:
-
创建
BukkitRunnable类 -
理解同步和异步任务及其使用时机
-
从
BukkitRunnable类运行任务 -
从
BukkitRunnable类调度延迟任务 -
从
BukkitRunnable类调度重复任务 -
编写一个名为
AlwaysDay的插件,该插件使用重复任务 -
将延迟任务添加到
Warper插件中 -
异步执行代码
创建 BukkitRunnable 类
我们将首先创建 AlwaysDay 插件。我们将为这个插件编写的代码将放在 onEnable 方法中。创建调度任务的第一步是创建一个 BukkitRunnable 类。这个类将包含非常少的代码行。因此,没有必要为它创建一个全新的 Java 文件。因此,我们将在 onEnable 方法中创建一个类。这可以通过以下代码行完成:
BukkitRunnable runnable = new BukkitRunnable();
通常,这段代码是有效的,因为你正在构造一个新类的实例。然而,BukkitRunnable是一个抽象类,这意味着它不能被实例化。抽象类的目的是提供一些基础代码,其他类可以扩展并在此基础上构建。一个例子是JavaPlugin类。对于你创建的每个插件,你都是从扩展JavaPlugin的类开始的。这允许你覆盖方法,例如onEnable,同时保留其他方法,如getConfig的当前代码。这与实现接口,如 Listener 类似。抽象类和接口之间的区别在于其目的。如前所述,抽象类是其他类扩展的基础。接口更像是一个框架,类可以在其中实现。接口不包含任何方法中的代码,因此,接口中的所有方法都必须有实现。对于抽象类,只有定义为abstract的方法必须被覆盖,因为它们不包含代码。因此,因为BukkitRunnable是一个抽象类,你会收到一个警告,要求你实现所有抽象方法。NetBeans 可以自动为你添加所需的方法。为你添加的新方法是run。当调度器运行你的任务时,将调用此方法。对于新的AlwaysDay插件,我们希望任务将每个世界的时间设置为中午,如下所示:
BukkitRunnable runnable = new BukkitRunnable() {
@Override
public void run() {
for (World world : Bukkit.getWorlds()) {
//Set the time to noon
world.setTime(6000);
}
}
};
注意
记住,在 Minecraft 服务器上,时间是以 tick 来衡量的。20 tick 等于 1 秒。tick 的测量方法如下:
0 ticks: 黎明
6,000 ticks: 中午
12,000 ticks: 黄昏
18,000 ticks: 午夜
查看关于BukkitRunnable类的 API 文档hub.spigotmc.org/javadocs/spigot/org/bukkit/scheduler/BukkitRunnable.html。注意,有六种运行此任务的方法,如下所示:
-
runTask
-
runTaskAsynchronously
-
runTaskLater
-
runTaskLaterAsynchronously
-
runTaskTimer
-
runTaskTimerAsynchronously
同步任务与异步任务
一个任务可以是同步执行或异步执行。简单来说,当一个同步任务执行时,它必须在服务器继续正常运行之前完成。异步任务可以在服务器继续运行的同时在后台运行。如果一个任务以任何方式访问 Bukkit API,那么它应该以同步方式运行。因此,你很少会以异步方式运行任务。异步任务的一个优点是它可以在不导致服务器卡顿的情况下完成。例如,将数据写入保存文件可以异步执行。在本章的后面,我们将修改Warper插件以异步保存其数据。至于AlwaysDay插件,我们必须以同步方式运行任务,因为它访问了 Minecraft 服务器。
从BukkitRunnable类运行任务
在BukkitRunnable类上调用runTask或runTaskAsynchronously会导致任务立即运行。你可能会使用这种情况是在从异步上下文运行同步任务或反之亦然。
从BukkitRunnable类稍后运行任务
在BukkitRunnable类上调用runTaskLater或runTaskLaterAsynchronously将延迟任务执行特定的时间。这个时间是以刻度为单位的。记住,每秒有 20 个刻度。在Warper插件中,我们将添加一个传送延迟,使玩家在运行传送命令后 5 秒被传送。我们将通过稍后运行任务来实现这一点。
从BukkitRunnable类运行任务计时器
在BukkitRunnable类上调用runTaskTimer或runTaskTimerAsynchronously会使任务每隔指定的时间间隔重复执行。任务会一直重复,直到被取消。任务计时器也可以延迟启动,以调整任务的初始运行。任务计时器可以用来定期保存数据,但到目前为止,我们将使用这种重复任务来完成AlwaysDay插件。
为插件编写重复任务
我们已经有了BukkitRunnable类。因此,为了运行任务计时器,我们只需要确定任务的延迟和延迟周期。我们希望延迟为 0。这样,如果插件启用时是夜晚,时间会立即设置为中午。至于周期,如果我们想保持太阳始终在正上方,我们可以每秒重复任务。任务只包含一行简单的代码。频繁重复它不会导致服务器卡顿。然而,每分钟重复任务仍然可以防止世界永远变暗,并且对计算机的压力会更小。因此,我们将任务延迟 0 个刻度,并每 1,200 个刻度重复一次。这导致以下代码行:
runnable.runTaskTimer(this, 0, 1200);
通过这种方式,我们启动了一个重复任务。当插件禁用时取消重复任务是良好的实践。为了完成这个任务,我们将BukkitTask存储为类变量,这样我们就可以稍后访问它来禁用它。一旦你在onDisable方法中取消了任务,以下代码给出了整个AlwaysDay插件:
package com.codisimus.alwaysday;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
public class AlwaysDay extends JavaPlugin {
BukkitTask dayLightTask;
@Override
public void onDisable() {
dayLightTask.cancel();
}
@Override
public void onEnable() {
BukkitRunnable bRunnable = new BukkitRunnable() {
@Override
public void run() {
for (World world : Bukkit.getWorlds()) {
//Set the time to noon
world.setTime(6000);
}
}
};
//Repeat task every 1200 ticks (1 minute)
dayLightTask = bRunnable.runTaskTimer(this, 0, 1200);
}
}
向插件中添加延迟任务
现在,我们将向Warper插件添加一个延迟。这将要求玩家在运行传送或回家命令后保持静止。如果他们移动太多,传送任务将被取消,他们不会传送。这将防止玩家在有人攻击他们或他们正在坠落时传送。
如果你还没有做,请在main类中添加一个warpDelay变量。这将在以下代码行中给出:
static int warpDelay = 5; //in seconds
这个时间将以秒为单位。我们将在代码中稍后将其乘以 20,以计算我们希望延迟任务的刻度数。
我们还需要跟踪正在传送过程中的玩家,以便我们可以检查他们是否在移动。为当前的warpers添加另一个变量。这将是一个HashMap,这样我们就可以跟踪哪些玩家正在传送以及将要运行的传送任务。这样,如果特定玩家移动,我们可以获取他们的任务并取消它。这可以在以下代码中看到:
private static HashMap<String, BukkitTask>warpers = new HashMap<>();//Player UUID -> Warp Task
代码包含三个新方法,必须将它们添加到main类中,以便安排传送任务、检查玩家是否有传送任务以及取消玩家的传送任务。代码如下:
/**
* Schedules a Player to be teleported after the delay time
*
* @param player The Player being teleported
* @param loc The location of the destination
*/
public static void scheduleWarp(final Player player, final Location loc) {
//Inform the player that they will be teleported
player.sendMessage("You will be teleported in "+ warpDelay + " seconds");
//Create a task to teleport the player
BukkitRunnable bRunnable = new BukkitRunnable() {
@Override
public void run() {
player.teleport(loc);
//Remove the player as a warper because they have already beenteleported
warpers.remove(player.getName());
}
};
//Schedule the task to run later
BukkitTask task = bRunnable.runTaskLater(plugin, 20L * warpDelay);
//Keep track of the player and their warp task
warpers.put(player.getUniqueId().toString(), task);
}
/**
* Returns true if the player is waiting to be teleported
*
* @param player The UUID of the Player in question
* @return true if the player is waiting to be warped
*/
public static boolean isWarping(String player) {
return warpers.containsKey(player);
}
/**
* Cancels the warp task for the given player
*
* @param player The UUID of the Player whose warp task will be canceled
*/
public static void cancelWarp(String player) {
//Check if the player is warping
if (isWarping(player)) {
//Remove the player as a warper
//Cancel the task so that the player is not teleported
warpers.remove(player).cancel();
}
}
在scheduleTeleportation方法中,请注意player和loc变量都是final的。这是在BukkitRunnable类中使用变量所必需的。这样做是为了确保在任务运行之前,值不会改变。此外,请注意runTaskLater方法调用返回一个BukkitTask,这是我们保存在HashMap中的内容。你可以通过查看cancelWarp方法来了解原因,该方法在执行之前移除指定玩家的BukkitTask并对其调用cancel方法。
在WarpCommand和HomeCommand类中,我们都会传送玩家。我们希望移除那行代码,并用对scheduleTeleportation方法的调用来替换它。功能添加即将完成。剩下要做的就是当warper移动时调用cancelWarp方法。为此,添加一个事件监听器来监听player move事件。这可以在以下代码中看到:
package com.codisimus.warper;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
public class WarperPlayerListener implements Listener {
@EventHandler (priority = EventPriority.MONITOR)
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
String playerUuid = player.getUniqueId().toString();
//We only care about this event if the player is flagged aswarping
if (Warper.isWarping(playerUuid)) {
//Compare the block locations rather than the player locations
//This allows a player to move their head without canceling thewarp
Block blockFrom = event.getFrom().getBlock();
Block blockTo = event.getTo().getBlock();
//Cancel the warp if the player moves to a different block
if (!blockFrom.equals(blockTo)) {
Warper.cancelWarp(playerUuid);
player.sendMessage("Warping canceled because you moved!");
}
}
}
}
不要忘记在onEnable方法中注册事件。
异步执行代码
我们可以通过异步将数据写入文件来改进Warper插件。这将有助于保持服务器主线程的流畅运行,不会出现延迟。
看一下当前的save方法。我们将数据添加到YamlConfiguration文件中,然后将配置写入文件。整个方法不能异步运行。将数据添加到配置必须同步进行,以确保在添加过程中不会被修改。然而,对配置的save方法调用可以是异步的。我们将整个try/catch块放在一个新的BukkitRunnable类中。然后我们将它作为一个任务异步运行。这个任务将被保存在Warper类的静态变量中。这可以在以下代码中看到:
BukkitRunnable saveRunnable = new BukkitRunnable() {
@Override
public void run() {
try {
//Write the configuration to our save file
config.save(new File(plugin.getDataFolder(), "warps.yml"));
} catch (Exception saveFailed) {
plugin.getLogger().log(Level.SEVERE, "Save Failed!", saveFailed);
}
}
};
saveTask = saveRunnable.runTaskAsynchronously(plugin);
现在,在数据保存期间,服务器的其余部分可以继续运行。
然而,如果我们尝试在之前的写入尚未完成时再次保存文件怎么办?在这种情况下,我们不在乎之前的任务,因为它现在正在保存过时的数据。我们将在创建BukkitRunnable类之前首先取消任务,然后开始一个新的任务。这将在以下代码中完成:
if (saveTask != null) {
saveTask.cancel();
}
这完成了Warper的此版本。如第九章中提到的,保存你的数据,这个插件有很多功能添加的潜力。你现在有了添加这些功能所需的知识。
摘要
你现在熟悉了 Bukkit API 的大部分复杂方面。有了这些知识,你可以编写几乎任何类型的 Bukkit 插件。尝试通过创建一个新的插件来应用这些知识。你可能尝试编写一个公告插件,该插件将在服务器上循环播放需要广播的消息列表。考虑所有 Bukkit API 概念以及你如何使用它们来为插件添加新功能。例如,使用公告插件,你可以做以下事情:
-
添加命令,允许管理员添加需要宣布的消息
-
添加权限来控制谁可以添加消息,甚至谁可以看到宣布的消息
-
添加一个
EventHandler方法来监听玩家登录时,以便可以向他们发送消息 -
添加一个
config.yml文件来设置消息应宣布的频率 -
添加一个保存文件来保存和加载将要宣布的所有消息
-
使用 Bukkit 调度器在服务器运行时重复广播消息
对于你制作的任何插件,考虑 Bukkit API 的每个部分,以找出通过添加更多功能来改进插件的方法。这无疑会使你的插件和服务器脱颖而出。
本书没有讨论一些主题,但它们足够简单;你可以通过阅读 API 文档自学如何使用它们。一些可以美化 Bukkit 插件的有趣功能是playSound和playEffect方法,它们位于World和Player类中。我鼓励你阅读它们并尝试自己使用。
你已经知道如何编写插件命令、玩家权限、事件监听器、配置文件、数据的保存和加载以及计划任务。剩下的事情就是想象如何使用这些新技能为 Bukkit 服务器创建一个伟大且独特的插件。


浙公网安备 33010602011771号