• 欢迎来到Minecraft插件百科!
  • 对百科编辑一脸懵逼?帮助:快速入门带您快速熟悉百科编辑!
  • 因近日遭受攻击,百科现已限制编辑,有意编辑请加入插件百科企鹅群:223812289

Bukkit/插件开发教程

来自Minecraft插件百科
Sanityko讨论 | 贡献2015年1月28日 (三) 00:13的版本 Bukkit Java文档
跳转至: 导航搜索
Icon-info.png
本页面已存在其他语言的内容,请协助翻译为本地化的中文。
  • 点击此处开始翻译。
  • 如本模板出现在原文存档页面,请注意更新主页面后,仍需要去除此处该模板。
  • 如当前页面已经没有需要翻译的内容,请删去待翻译模板。
  • 有标题的大篇幅文章,如果短时间内无法全部翻译,请先把所有的标题翻译出来,以便之后的贡献者选择与翻译章节内容。

本页面英文原文内容来源 [1]

目录

介绍

这篇内容丰富的教程旨在帮助你学会如何开发Bukkit插件.这一篇教程不可能完全涉及到Bukkit中所有的可能性,而是最基本的一般概述. 前提是你懂得Java这门语言,并在IDE中设置好你的工作区,此教程介绍了大多数Bukkit插件所必需的语法结构.

学习Java

理解这些教程需要具备Java这门编程语言的基本知识.如果你刚开始学习Java或者需要重温一下相关知识,下面有一个并不完整的网站列表可供参考.

Oracle's Java Tutorials - 官方教程

开发环境

在编写一个插件前 (或学习Java前) 你需要配置一个开发者环境,这个包括但不限于安装IDE (Integrated Development Environment,集成开发环境)。接下来的教程中有关于Eclipse上IDE的操作指南。

更多信息,请参见 建立工作区

必须下载Java开发者使用的Eclipse构建版本, 不是 Java EE开发者的版本。 Java EE 开发者的版本不包含此教程需要的Maven支持。

新建插件开发项目

创建一个新的项目

在开始工作之前,你需要先在Eclipse中配置好工作区和文件. 打开Eclipse,然后依次点击File -> New -> Project:来创建一个新的项目.

Newproject.png

现在,打开Maven文件夹, 然后选择Maven Project.点击next,之后在下一个菜单中选择Create a simple project, 再次点击Next: 如果你看不到Maven文件夹, 那么你需要下载m2eclipse

Newproject2.png

现在,你需要给你的组用户命名,就像下面这样:

  • 如果你拥有一个域名,package则填写逆转的域名地址.
    • 例如: i-am-a-bukkit-developer.com 你的package地址即是com.i_am_a_bukkit_developer source
    • 避免使用一个不属于你自己的域名.
  • 没有域名? 这里有几种方法可供选择。
    1. 在资源管理站点创建一个用户,比如GitHub或是sourceforge
      • 对于使用GitHub的用户, 请参照这里的说明 之后你将获得一个子域名,所以你的package地址是io.github.<username>
    2. 使用你的邮箱. 例如: <username>@gmail.com格式的邮箱应输入com.gmail.<username>
    3. 最不济的方法.: 使用独特的组名命名方式,这应当是你最后的解决方法。

以下几个地址不能作为package中的地址前缀:

  • org.bukkit
  • net.bukkit
  • com.bukkit
  • net.minecraft

完成基础的组名以后,你需要在最后加上插件的名称. 在这里用 GitHub页面作为讲解的实例. 如果你创建了一个名为 TestPlugin的插件,那么完整的组名是io.github.<username>, 你的工程名也是 TestPlugin. 至于版本,默认即可,稍后可以修改。 完成向导:

Newproject3.png

如果这是你首次使用Eclipse, 点击右上角的"X" 来关闭Welcome提示页面. 现在,你的窗口视图看起来就像下面的图片这样:

Eclipsemain.png

点击工程名称右边的箭头来进行下一步,现在我们正式开始。

添加Bukkit API

在开发插件之前,你需要添加 Bukkit API库文件到你的项目,作为一个dependency, 你也可以添加其他你想实用的API.

找到项目文件夹中部的pom.xml并双击进行编辑. 点击pom.xml中部的 tab,你将会看到下图所示内容:

Pomeditor.png

如果你想使用Java 6以上版本的语言特性,你需要指定搭建项目的Java版本.复制并粘贴以下内容(设定项目只能使用Java 7及以下的版本)到 </project>之前:

   <build>
      <plugins>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-compiler-plugin</artifactId>
              <configuration>
                  <source>1.7</source>
                  <target>1.7</target>
              </configuration>
          </plugin>
      </plugins>
   </build>

你或许想使用其他版本,例如 1.8 使用Java 8. 请注意 根据MCStats数据统计, 大多数服主选择了Java 7, 所以使用Java 8会使许多服务器无法运行你的插件. 如果你使用Java 1.7的特性, Eclipse将会在报错代码"error"中建议你更改语言版本.同意即可.

在位于代码中段的</project>之前粘贴以下内容(这段代码告诉Eclipse关于Bukkit's repository的地址):

   <repositories>
       <repository>
           <id>bukkit-repo</id>
           <url>http://repo.bukkit.org/content/groups/public/</url>
       </repository>
   </repositories>

然后继续在 </project>前粘贴以下内容 (这段代码告诉Eclipse搭建插件的Bukkit版本):

   <dependencies>
       <dependency>
           <groupId>org.bukkit</groupId>
           <artifactId>bukkit</artifactId>
           <version>1.7.2-R0.3</version>
           <type>jar</type>
           <scope>provided</scope>
       </dependency>
   </dependencies>

你也可以修改这里的Bukkit版本.你可以在here这里通过查看 maven-metadata.xml文件下的版本列表来获取可用的服务端版本号.

当你完成上述步骤后,pom.xml中的内容应该是这样:

Finishedpom.png

通过菜单 File -> Save或者按住 Ctrl + S 来保存pom.xml文件 . 之后, 右键项目标题,然后依次选择 Maven -> Update Project.

Bukkit Java文档

如果你曾经使用过Eclipse和Java,你会知道当你将鼠标准心移至class或者 method中的代码部分时,一个含有相关文档内容的黄色小框会弹出来. 这就是(Java的)Java文档。

你可以Oracle website 在线获取它.。Bukkit同样拥有文档,这些文档内容包含了关于API中method和class的有用描述。

你可以在找到它 (Beta版的Java文档在找, 新开发出来的Java文档在  

) (明明都一样好吗)。为了在Eclipse里使用Java文档,这样的话当你的鼠标停留在Bukkit的class和method上时,

(Bukkit的)Java文档就会冒出来。 
首先右键在侧边栏里的"Maven Dependencies"下面的Bukkit jar,选择"Properties".选中窗口左边的Javadoc Location, 并粘贴如下网址 

http://jd.bukkit.org/apidocs/ (或者上面的测试版/最新版 Java文档链接也好) 到"Javadoc URL"下面的输入框内. 就像这样: 

Bukkitjavadocs.png

点击 "Validate",然后再点击"OK"就完成了.现在Bukkit Javadocs已经连接到Bukkit提供的资源,同时你也可以通过Eclipse来获得帮助类文档信息了.

创建一个包

现在你需要创建一个包,它将储存所有我们需要使用的Java类文件. 右键展开src/main/java折叠并选择 New > Package:

Newpackage.png

你的包名 put your group name, then a period, and then your artifact name in lowercase. For example, if your group name is io.github.name and your artifact name is TestPlugin, your package name would be io.github.name.testplugin.

创建插件的类文件

既然我们已经建立好了我们的项目,我们接下来可以开始添加类文件以及制作插件了. 插件的主类指的是拓展(extends)JavaPlugin的类文件. 在你的插件中只能有一个类文件拓展JavaPlugin,无论直接还是间接. 先创建你的主类文件并保持名称与插件名一致是个很好的习惯.右键你之前新建的包,并选择  New > Class.你将会创建一个新的类文件,就像下面这样

package {$GroupName}.{$ArtifactName};

import org.bukkit.plugin.java.JavaPlugin;

public final class {$ArtifactName} extends JavaPlugin {
   
}

警告: 插件绝对不应该调用自己的构造函数并实例化

创建plugin.yml

你已经创建了项目和主类,如果你想要Bukkit能够加载你的插件,你还必须创建 plugin.yml 文件. 这个文件包含有基础的插件信息,如果缺失这个文件,你的插件也将会失效. 此时你需要右键src/main/resources. 选择New > File. 命名为"plugin.yml" 并右键完成新文件的创建.Eclipse会打开默认的文本编辑窗口来显示plugin.yml文件中空白的内容并提供编辑. (Hint: 如果你想要使得你的工作空间更加规整,关闭文本编辑器并将plugin.yml 文件拖到主工作空间(拖到文件目录的右边) 之后你就可以在eclipse中编辑文件了.)

有三个基础的内容需要在plugin.yml写明.

name: 插件名称.
main: 插件主类的完整,合法名称 .
version: 插件当前版本号.

最简单的 plugin.yml 文件内就像这样 :

name: {插件名称}
main: {包名}.{主类}

version: {版本号}</source>

PS:插件的包名经常会包括插件的名称,所以看到这个的时候不要感到惊讶。
   你的主类是否是你的插件名取决于你之前的命名方式,时刻记住这一点很重要。

更多例子, 请看#Example_Files_and_Templates

此时你的插件已经可以被Bukkit加载了,同时服务端日志也会开始记录你的插件. 不过它现在什么用都没有!

onEnable() and onDisable()方法

这些方法将在插件启用与卸载时生效. 默认情况下,你的插件在被加载时会调用这些方法,所以你可以在这里注册你需要用到的事件和提供一些调试信息. onEnable() 方法会在插件启用时被调用, 需要包含初始化插件的逻辑语句. onDisable() 方法会在插件卸载时被调用,需要包含清理(clean up)插件的逻辑语句and associated state. Additionally plugins can override the onLoad() method to perform additional logic when the plugin loads.

onEnable() and onDisable()方法介绍

在前面的章节中,我们在主类创建了onEnable()onDisable() 方法. 此时,这些代码看起来就像下面这样

package {$TopLevelDomain}.{$Domain}.{$PluginName};

import org.bukkit.plugin.java.JavaPlugin;

public final class {$PluginName} extends JavaPlugin {
    @Override
    public void onEnable() {
        // TODO Insert logic to be performed when the plugin is enabled
    }
    
    @Override
    public void onDisable() {
        // TODO Insert logic to be performed when the plugin is disabled
    }
}

这些方法已经创建,但目前它们还没有任何作用. 注意: 不需要添加代码来实现输出信息"{$PluginName} has been enabled!" ,因为bukkit会自动输出此类信息 有关更多事件的信息请查看 here.

发送提示信息

插件能够输出信息至控制台与服务器日志. It can accomplish this by invoking the correct method from the plugin's logger. First we must invoke the getLogger() method to retrieve the logger associate with this plugin. Then we can start logging.

We will write to the log when onEnable() method is called. We can do that by inserting the following line into the onEnable() method.

getLogger().info("onEnable has been invoked!");

You can then do the same inside onDisable(), making sure to change the message.

你的主类文件就像这样:

package {$TopLevelDomain}.{$Domain}.{$PluginName};

import org.bukkit.plugin.java.JavaPlugin;

public final class {$PluginName} extends JavaPlugin {
	@Override
	public void onEnable() {
		getLogger().info("onEnable has been invoked!");
	}

	@Override
	public void onDisable() {
		getLogger().info("onDisable has been invoked!");
	}
}

防止重载后插件失效

你需要知道的是,插件并不只会伴随服务器关闭、启动而重载,你的插件也可能会被其他插件启用、关闭,或是在服务器运行时使用/reload命令. 假设服务器启动之后一会儿后,你的插件才被启用,这时便是危险的,因为服务器中可能已经有玩家在线、额外的世界被加载以及很多预想不到的不同之处(与"插件同服务器同时加载"的情况相比较).

举个例子来说:

  • 你有一个用来储存HashMap中的玩家登入信息的插件
  • 你希望该信息对所有玩家都可见
  • 一名管理员使用/reload命令
  • 你的插件被卸载,所有储存的数据都丢失了
  • 你的插件被再次启用时,一些玩家已经在线
  • 这些玩家在HashMap并没有储存的信息
  • 你试图重新获得他们的信息,但是失败了!

为了插件重载以后能够正常工作, 你需要在插件启用时加载所有已经在线的玩家的信息并将之储存在HashMap中.

for (Player player : Bukkit.getServer().getOnlinePlayers()) {
    playerList.put(player.getName(), playerData(player));
}

监听器

监听器是一种方法被调用来对事件作出反应的类.所有的监听器使用 org.bukkit.event.Listener接口.更多有关监听器创建的细节,

请查看: Event API Reference

指令

onCommand() 方法

现在你已经知道如何注册一个事件并做出响应,但是如果你只是想要在命令输入之后做出响应呢? 你可以使用 onCommand方法.

这个代码的作用是当玩家输入“/”时,监听该操作并执行相关语句"/" . 举个例子来说.输入 "/do something" 将会执行 onCommand方法.

此种情况下,因为没有特定的行为被编程,所以并不会发生任何事情。
请避免使用和bukkit所提供的指令重名的指令, 然后深思你的指令名的唯一性。 

例如.指令"give"已经被好几个插件使用了, 如果你执行了另外一个"give"指令, 你的插件将会和这些插件冲突。
你必须在插件的"plugins.yml"注册你的指令 否则这个方法将不会被触发。
onCommand方法必须返回一个布尔值(true或false)。 

如果返回值是true,你不会看到什么明显的事情发生。 

但如果返回值是false,则会返回你的plugin.yml里的'usage:property'然后发送给命令使用者. 

当使用 onCommand方法时, 你需要填写4个参数.

*CommandSender sender - 发送命令的对象

*Command cmd - 被执行的指令

*String commandLabel - 被执行指令的别名

*String[] args - 该指令的自变量数组。

例如.指令 /hello abc def 中, abc 会被存放进args[0]中, def 被存放进args[1]中。

设置命令

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
    if (cmd.getName().equalsIgnoreCase("basic")) { // 如果玩家输入了/basic则执行如下内容...
    // 所需要执行的事(此处略)
        return true;
    } //如果以上内容成功执行,则返回true 
    // 如果执行失败,则返回false.
    return false;
}
每当使用onCommand方法时, 在每个方法后面都加上一个 return false; 是一个惯例。 

返回false会显示在plugin.yml里设置的usage信息 (看下面). 用这种方法的话,一旦执行出错便会显示usage信息。
每当方法return一个值的时候,这个方法就会退出,所以return true的时候,在它下面的代码就会被跳过, 

除非return语句在一个if的嵌套中,或者类似的嵌套情况。 
.equalsIgnoreCase("basic")代表忽略英文大小写. 在这种情况下,字符串"BAsIc" 和 "BasiC"和 basic相同,代码会被照常执行。

和以前一样,加两行import在你的java文件头

import org.bukkit.command.Command;

import org.bukkit.command.CommandSender;

在Plugin.yml中添加你的指令

你需要添加你的指令到 plugin.yml 文件里. 如下是指令/basic添加到 plugin.yml的例子(请在plugins.yml的末尾添加如下代码):

commands:

  basic:
     description: This is a demo command.
     usage: /<command> [player]
     permission: <plugin name>.basic

permission-message: You don't have <permission></source>

  • basic - 指令名称
  • description - 指令描述
  • usage - 在oncommand方法return false后显示的用法提示。尽量简洁, 使别人能够理解指令是什么以及如何使用它。
  • permission - 被一些显示帮助信息的插件使用,用来决定向玩家显示哪个指令(完全不对劲吧!)。
  • permission-message - 当玩家使用了这个指令而没有权限时输出的信息。


PS:yml文件使用两个空格作为制表符,使用tab键输入制表符会导致错误。

控制台指令vs玩家指令

你可能注意到了上文的CommandSender sender参数. CommandSender 是个Bukkit接口,

它有两个(对插件编写者)有用的子类:PlayerConsoleCommandSender. 
所以当你编写插件的时候, 确定这个指令从控制台发出后能完全正常工作是十分必要的,

那些只能由在线玩家执行的指令 只能 由一个在线玩家执行。

有的插件在判断命令执 行对象不是玩家时仅仅用return进行处理 (比如当有人从控制台发出指令), 

即便这些指令能够在控制台完美运行(比如改变天气的指令)。

一个判断sender的方法:

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        if (cmd.getName().equalsIgnoreCase("basic")) { //如果玩家输入了/basic则执行如下内容...
            // 所需要执行的事(此处略)
            return true;
        } else if (cmd.getName().equalsIgnoreCase("basic2")) {
            if (!(sender instanceof Player)) { //如果sender与Player类不匹配
                sender.sendMessage("This command can only be run by a player.");
            } else {
                Player player = (Player) sender;
                   // 所需要执行的事(此处略)
            }
                return true;
        }
        return false;
}
在这个例子里,指令basic能由任何人发出 - 一个在线的玩家, 或者服务器后台的op. 

但是指令basic2只能由一个在线的玩家发出. 
大体上说,你应该允许尽可能多的指令在控制台和玩家聊天框里都正常执行. 

需要在线玩家执行的指令可以使用以上例子中的机制来检测CommandSender是一个玩家. 

很多指令广泛依靠于一些玩家的属性(例如可以被tp或被给予物品等等)。
如果你想更进一步, 你可以对你的指令的自变量做一些额外的检测, 例如传送指令在提供玩家ID的情况下(也只有在这种情况下)可以在控制台被执行。

使用独立的 CommandExecutor class

以上的例子必须把 onCommand() 方法放在插件的主类里。

对于小插件来说,这是极好的,但如果你要写一个大点的插件,把你的 onCommand()方法放在独立的类里将会很有意义。

幸运的是,这并不难: 
  • 在你的包里创建一个新类. 命名为MyPluginCommandExecutor之类的 (当然,要把MyPlugin替换成你制作的插件的名字)。 这个类 必须 继承Bukkit 的CommandExecutor接口。
  • 在你的插件的onEnable()方法中,你需要实例化你创建的命令执行类(CommandExecutor class), 然后做一个像下面一样的调用:getCommand("basic").setExecutor(myExecutor);
其中"basic"是我们需要操控的指令, myExecutor 是我们创建的实例.

废话少说直接吔我例子:

MyPlugin.java (插件的主类):

@Override
public void onEnable() {
        // 如果你没有在plugins.yml注册过指令的话,此处会抛出空指针异常!
        this.getCommand("basic").setExecutor(new MyPluginCommandExecutor(this));
}
MyPluginCommandExecutor.java(独立CommandExecutor类): 
public class MyPluginCommandExecutor implements CommandExecutor {
        private final MyPlugin plugin;

        public MyPluginCommandExecutor(MyPlugin plugin) {
            this.plugin = plugin; // Store the plugin in situations where you need it.
        }

        @Override
        public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
            // 和以前一样执行就好…
        }
}
请注意我们是如何把主类里的对象指向MyPluginCommandExecutor的. 这允许我们轻易的使用主类对象的方法(如果我们需要的话)。
这么做的话, 我们便能更好的组织代码 - 如果 onCommand() 方法又大又复杂, 它就能被划分为子类,从而不使插件的主类显得凌乱。
PS:如果你的插件含有多个指令的话, 你需要为每一个指令单独设立一个commandexecutor.

写一个安全的指令

当写一个指令的时候,别进行任何假设(很重要), 例如假设执行者一定是个玩家. 请牢记以下原则:

在命令执行前确定发出者是玩家

使用简单的例子来进行检测:

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        if (sender instanceof Player) {
            Player player = (Player) sender;
            // do something
        } else {
            sender.sendMessage("You must be a player!");
            return false;
        }
            // do something
        return false;
}

检测自变量个数

别老是认为玩家都能打对正确数量的自变量. 
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        if (args.length > 4) {
            sender.sendMessage("Too many arguments!");
            return false;
        } 
        if (args.length < 2) {
            sender.sendMessage("Not enough arguments!");
            return false;
        }
}

在获取玩家之前先检测他们是否在线

有时你想通过命令发出者打出的ID来获取其他玩家,请确保他们在线再说!
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        Player target = (Bukkit.getServer().getPlayer(args[0]));
        if (target == null) {
            sender.sendMessage(args[0] + " is not online!");
            return false;
        }
        return false;
}
如果你想对离线玩家进行操作,OfflinePlayer类提供了基础操作的方法.

插件的配置/设置

Bukkit API 为插件提供了一种简便的方法,用来管理用户自定义的设置。同时,通过这种方法也可以储存数据。

详情请查看: Configuration API Reference

Permissions

With the new Bukkit API for permissions, they couldn't be easier. To find out if a player has a particular permission use the following:

if (player.hasPermission("some.pointless.permission")) {
   //Do something
} else {
   //Do something else
}

You can also find if a permission has been set or not (equivalent to Java's null) with the following function:

boolean isPermissionSet(String name)

You may be wondering why there aren't any groups. The answer to that is because they aren't really needed. Previously one of the main uses for groups was to format chat messages. That however can be done just as easily with permissions. Inside your chat plugin's config you would define associations between permissions and prefixes. For example the permission "someChat.prefix.admin" would correspond to the prefix [Admin]. Whenever a player speaks with that permission their name will be prefixed with [Admin].

Another common usage might be to send a message to all users within a group. Again however this can be done with permissions with the following:

for (Player player: Bukkit.getServer().getOnlinePlayers()) {
    if (player.hasPermission("send.receive.message")) {
        player.sendMessage("You were sent a message");
    }
}

Finally you may be asking, well how do I set and organise player's permissions if there are no groups? Although the bukkit API doesn't provide groups itself, you must install a permission provider plugin such as permissionsBukkit to manage the groups for you. This API provides the interface, not the implementation.

Configuring your permissions

If you want more control over your permissions, for example default values or children then you should consider adding them to your plugin.yml. This is completely optional, however it is advised. Below is an example permissions config that would be appended to the end of your existing plugin.yml:

permissions:
    doorman.*:
        description: Gives access to all doorman commands
        children:
            doorman.kick: true
            doorman.ban: true
            doorman.knock: true
            doorman.denied: false
    doorman.kick:
        description: Allows you to kick a user
        default: op
    doorman.ban:
        description: Allows you to ban a user
        default: op
    doorman.knock:
        description: Knocks on the door!
        default: true
    doorman.denied:
        description: Prevents this user from entering the door

Firstly, each permission your plugin uses is defined as a child node of the permissions node. Each permission can then optionally have a description, a default value, and children.

Defaults

By default when a permission isn't defined for a player hasPermission will return false. Inside your plugin.yml you can change this by setting the default node to be one of four values:

  • true - The permission will be true by default.
  • false - The permission will by false by default.
  • op - If the player is an op then this will be true.
  • not op - If the player is not an op then this will be true.

Children

Before now you will probably be used to the * permission to automatically assign all sub permissions. This has changed with the bukkit API and you can now define the child permissions. This allows for a lot more flexibility. Below is an example of how you do this:

permissions:
    doorman.*:
        description: Gives access to all doorman commands
        children:
            doorman.kick: true
            doorman.ban: true
            doorman.knock: true
            doorman.denied: false

Here the doorman.* permission has several child permissions assigned to it. The way child permissions work is when doorman.* is set to true, the child permissions are set to their values defined in the plugin.yml. If however doorman.* was set to false then all child permissions would be inverted.

Setting your own permissions

If you wish to know about developing your own permissions plugins (Ones that actually set permissions) then check out the tutorial on Developing a permissions plugin.

Scheduling Tasks and Background Tasks

Currently, Minecraft servers operate nearly all of the game logic in one thread, so each individual task that happens in the game needs to be kept very short. A complicated piece of code in your plugin has the potential to cause huge delays and lag spikes to the game logic, if not handled properly.

Luckily, Bukkit has support for scheduling code in your plugin. You can submit a Runnable task to occur once in the future, or on a recurring basis, or you can spin off a whole new independent thread that can perform lengthy tasks in parallel with the game logic.

There is a separate Scheduler Programming tutorial which introduces the Scheduler, and gives more information on using it to schedule synchronous tasks, and on kicking off asynchronous tasks in Bukkit.

Block Manipulation

The easiest way to create blocks is to get an existing block and modify it. For example, if you want to change the block that is located five blocks above you, you would first have to get your current location, add five to your current y-coordinate, and then change it. For example:

@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
    // Get the player's location.
    Location loc = event.getPlayer().getLocation();
    // Sets loc to five above where it used to be. Note that this doesn't change the player's position.
    loc.setY(loc.getY() + 5);
    // Gets the block at the new location.
    Block b = loc.getBlock();
    // Sets the block to type id 1 (stone).
    b.setType(Material.STONE);
}

The above code gets the player's location, gets the block five above it, and sets it to stone. Note that once you have a Block, there are other things you can do besides set its type. Consult the JavaDocs for more information.

You can use a similar concept to generate buildings and individual blocks programmatically through the use of algorithms. For example, to generate a solid cube, you could use nested for loops to loop over an entire cube and fill it in.

public void generateCube(Location loc, int length) {
    // Set one corner of the cube to the given location.
    // Uses getBlockN() instead of getN() to avoid casting to an int later.
    int x1 = loc.getBlockX(); 
    int y1 = loc.getBlockY();
    int z1 = loc.getBlockZ();

    // Figure out the opposite corner of the cube by taking the corner and adding length to all coordinates.
    int x2 = x1 + length;
    int y2 = y1 + length;
    int z2 = z1 + length;

    World world = loc.getWorld();

    // Loop over the cube in the x dimension.
    for (int xPoint = x1; xPoint <= x2; xPoint++) { 
        // Loop over the cube in the y dimension.
        for (int yPoint = y1; yPoint <= y2; yPoint++) {
            // Loop over the cube in the z dimension.
            for (int zPoint = z1; zPoint <= z2; zPoint++) {
                // Get the block that we are currently looping over.
                Block currentBlock = world.getBlockAt(xPoint, yPoint, zPoint);
                // Set the block to type 57 (Diamond block!)
                currentBlock.setType(Material.DIAMOND_BLOCK);
            }
        }
    }
}

This method will construct a 3D cube or cuboid with the given length and starting point. As for deleting blocks simply follow the same method for creating them but set the ID to 0 (air).

(Player) Inventory Manipulation

This section mostly covers player inventory manipulation, but the same applies to chest inventory manipulation as well if you find out how to get a chest's inventory :P. Here is a simple example of inventory manipulation:

public void onPlayerJoin(PlayerJoinEvent evt) {
    Player player = evt.getPlayer(); // The player who joined
    PlayerInventory inventory = player.getInventory(); // The player's inventory
    ItemStack itemstack = new ItemStack(Material.DIAMOND, 64); // A stack of diamonds
        
    if (inventory.contains(itemstack)) {
        inventory.addItem(itemstack); // Adds a stack of diamonds to the player's inventory
        player.sendMessage("Welcome! You seem to be reeeally rich, so we gave you some more diamonds!");
    }
}

So inside onPlayerJoin we first make a few variables to make our job easier: player, inventory and itemstack. Inventory is the player's inventory and itemstack is a ItemStack that has 64 diamonds. After that we check if the player's inventory contains a stack of diamonds. If the player has a stack of diamonds, we give him/her another stack with inventory.addItem(itemstack) and send a message. So inventory manipulation isn't actually that hard, if we wanted we could remove the stack of diamonds by simply replacing inventory.addItem(itemstack) with inventory.remove(itemstack) and change the message a little bit. Hopefully this helped!

Item Manipulation

When dealing with items in the code, you use the ItemStack class for looking up and setting all information on that stack.

Enchantments

To enchant an item you must first know the Item Code and the Effect ID. Enchantments themselves cannot be instantiated (new Enchantment() won't work) because they're abstract, so you must use an EnchantmentWrapper. If you want to enchant items that can't be enchanted inside normal SMP, use addUnsafeEnchantment() instead of addEnchantment()

int itemCode = 280;  //use the item code you want here
int effectId = 20;  //use the enchantment code you want here
int enchantmentLevel = 100;

ItemStack myItem = new ItemStack(itemCode);  //new item of item code
Enchantment myEnchantment = new EnchantmentWrapper(effectId);  //new enchantment of effect id
myItem.addEnchantment(myEnchantment, enchantmentLevel);  //enchant the item

ItemMeta

You can set the display name of an item by doing this.

String myDisplayName = "Awesome Sword"; //use the displayname you want here
 
ItemStack myItem = new ItemStack(Material.DIAMOND_SWORD);  //your item
ItemMeta im = myItem.getItemMeta(); //get the itemmeta of the item
im.setDisplayName(myDisplayName); //set the displayname
myItem.setItemMeta(im); //give the item the new itemmeta

You can also set the lores of an item. The lores are the small annotations on an item, like "+5 attack damage" on a stone sword.

List<String> lores = new ArrayList<String>();
lores.add("Example lore", "this one comes on line 2");

ItemStack myItem = new ItemStack(Material.DIAMOND_SWORD);  //your item
ItemMeta im = myItem.getItemMeta(); //get the itemmeta of the item again
im.setLore(lores); //add the lores of course
myItem.setItemMeta(im); //give the item the new itemmeta

Maps, and Sets, and Lists, Oh My!

Besides the Map/HashMap classes, Java offers many other data structures. They offer these different classes because there are times when a Map is not the most appropriate. Here's a separate page for discussing Java data structure classes in more detail.

HashMaps and How to Use Them

Keep in mind to never use a player in a hashmap! You need to use Strings instead. So use "p.getName()" to add, remove or check if a list contains a player. Saving a player as an object causes huge memory leaks.

When making a plugin you will get to a point where just using single variables to state an event has happened or a condition has been met will be insufficient, due to more than one player performing that action/event.

This was the problem I had with one of my old plugins, Zones, now improved and re-named to Regions. I was getting most of these errors because I didn't consider how the plugin would behave on an actual server with more than one on at any given time. I was using a single boolean variable to check whether players were in the region or not and obviously this wouldn't work as the values for each individual player need to be separate. So if one player was in a region and one was out the variable would constantly be changing which could/would/did cause numerous errors.

A HashMap is an excellent way of doing this. A HashMap is a way of mapping/assigning a value to a key. You could set up the HashMap so that the key is a player and the value could be anything you want, however the useful things with HashMaps is that one key can only contain one value and there can be no duplicate keys. So say for example I put "adam" as the key and assigned a value of "a" to it. That would work as intended, but then say afterwards I wanted to assign the value of "b" to key "adam" I would be able to and would get no errors but the value of "a" assigned to key "adam" in the HashMap would be overwritten because HashMaps cannot contain duplicate values.

Defining a HashMap

public Map<KeyType, DataType> HashMapName = new HashMap<>(); //Example syntax

// Example Declaration
public Map<String, Boolean> pluginEnabled = new HashMap<>();
public Map<String, Boolean> isGodMode = new HashMap<>();

Keep that code in mind because we will be using it for the rest of the tutorial on HashMaps. So, for example lets create a simple function which will toggle whether the plugin has been enabled or not. Firstly, inside your on command function which I explained earlier you will need to create a function to send the player name to the function and adjust the players state accordingly.

So inside on command you'll need this, the function name can be different but for the sake of simplicity it's best if you keep it the same.

Player player = (Player) sender;
togglePluginState(player);

This code above will cast the value of sender to player and pass that argument to the function togglePluginState(). But now we need to create our togglePluginState() function.

public void togglePluginState(Player player) {
    // Notice how we use the player name as the key here,
    // not the player object
    String playerName = player.getName();
    if (pluginEnabled.containsKey(playerName)) {
        if (pluginEnabled.get(playerName)) {
            pluginEnabled.put(playerName, false);
            player.sendMessage("Plugin disabled");
        } else {
            pluginEnabled.put(playerName, true);
            player.sendMessage("Plugin enabled");
        }
    } else {
        pluginEnabled.put(playerName, true); //If you want plugin disabled by default change this value to false.
        player.sendMessage("Plugin enabled");
    }
}

Now, what this code is doing is checking if the HashMap first contains the key player, so if it has been put into the HashMap, if it is then we check the value of the HashMap key by get(player); if this is true then set value to false and send the player a message, else if the value is false then do the opposite, set the value to true and send a message again. But if the HashMap does not contain the key player then we can assume that this is their first run/use so we change the default value and add the player to the HashMap.

More Ideas for HashMaps

A HashMap (or really any kind of Map in Java) is an association. It allows quick and efficient lookup of some sort of value, given a unique key. Anywhere this happens in your code, a Map may be your solution.

Here are a few other ideas which are ideally suited to using Maps. As you will see, it doesn't have to be data that you store per player, but can be any kind of data that needs to be "translated" from one form to another.

Data Value Lookups
public Map<String, Integer> wool_colors = new HashMap<>();

// Run this on plugin startup (ideally reading from a file instead of copied out row by row):
wool_colors.put("orange", 1);
wool_colors.put("magenta", 2);
wool_colors.put("light blue", 3);
   ..
wool_colors.put("black", 15);

// Run this in response to user commands - turn "green" into 13
int datavalue = 0;
if (wool_colors.containsKey(argument)) {
    datavalue = wool_colors.get(argument);
} else {
    try { datavalue = Integer.parseInt(argument); }
    catch (Exception e) {}
}

Saving/Loading a HashMap

Once you know how to work with HashMaps, you probably want to know how to save and load the HashMap data. Saving and loading HashMap data is appropriate if

  • you don't want an administrator to edit the data manually
  • you need to save data in binary format (too complex to organize for YAML)
  • you want to avoid parsing block names and/or other objects from freeform text

This is very simple way how to save any HashMap. You can replace HashMap<String, Integer> with any type of HashMap you want. Let's take an example HashMap with code to save it:

        HashMap<String, Integer> mapToSave = new HashMap<String,Integer>();
        public void save(HashMap<String, Integer> map, String path) {
	try {
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
		oos.writeObject(map);
		oos.flush();
		oos.close();
		//Handle I/O exceptions
	} catch(Exception e) {
		e.printStackTrace();
	}
}

// ...

save(mapToSave, getDataFolder() + File.separator + "example.bin");

You can see it's really easy. Loading works very very similar but we use ObjectInputStream instead of ObjectOutputStream ,FileInputStream instead of FileOutputStream,readObject() instead of writeObject() and we return the HashMap.

public HashMap<String, Integer> load(String path) {
	try {
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
		Object result = ois.readObject();
		//you can feel free to cast result to HashMap<String, Integer> if you know there's that HashMap in the file
		return (HashMap<String, Integer>)result;
	} catch(Exception e) {
		e.printStackTrace();
	}
}

// ...

HashMap<Integer, String> loadedMap;

String path = getDataFolder() + File.separator + "example.bin";
File file = new File(path);

if (file.exists()) { // check if file exists before loading to avoid errors!
	loadedMap  = load(path);
}

You can use this "API" for saving/loading HashMaps, ArrayLists, and all Objects which implement Serializable or Externalizable interface.

/** SLAPI = Saving/Loading API
 * API for Saving and Loading Objects.
 * Everyone has permission to include this code in their plugins as they wish :)
 * @author Tomsik68<tomsik68@gmail.com>
 */
public class SLAPI
{
	public static <T extends Object> void save(T obj,String path) throws Exception
	{
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
		oos.writeObject(obj);
		oos.flush();
		oos.close();
	}
	public static <T extends Object> T load(String path) throws Exception
	{
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
		T result = (T)ois.readObject();
		ois.close();
		return result;
	}
}

Example implementation of this API: I'm skipping some part of code in this source

public class Example extends JavaPlugin {
	private ArrayList<Object> list = new ArrayList<>();

	@Override
	public void onEnable() {
            try {
		list = SLAPI.load("example.bin");
            } catch(Exception e) {
                //handle the exception
                e.printStackTrace();
            }
	}

	@Override
	public void onDisable()	{
            try {
		SLAPI.save(list,"example.bin");
            } catch(Exception e) {
                 e.printStackTrace();
            }
	}
}

Note #1: This will work un-modified with almost all well-known Java types like Integer, String, HashMap. It will also work for some Bukkit types as well. If you're writing your own data object classes, and you may want to save their state using this technique, you should read about Java's Serializable or Externalizable interface. The only difference between Externalizable and Serializable is, that Serializable automatically takes all of class's fields and tries to serialize them, while Externalizable allows you to define method for reading and writing the Object. It's easy to add to your code, and it will make your data persistent with very little work required. No more parsing!

Note #2: This API doesn't support changes. Once you change something in the class, data files saved with older version of your plugin won't load correctly.

Tips & Examples

1.) Simplify your save structure

Try to use as much simple types as possible. E.g. if you want to save player, save their UUID instead. If you want to save world, save its UUID. If you want to save location, save x,y,z world UUID. DO NOT DIRECTLY SAVE BUKKIT TYPES!

2.) Save version number along with data

You should always remember, that you don't know what you'll be saving in the same file tomorrow. Will you ever migrate this file because of newer version of your plugin, bukkit, or minecraft? You don't know!

3.) Migrate older files

If your plugin finds older version of some file, it should update the file accordingly and change version number.

Metadata

Bukkit is trying to make plugin development as easy as possible, so HashMaps with key of type Player, Entity, World or even a Block were replaced by Metadata. Metadata is some kind of alternative to HashMap. It allows you to add custom "fields" to Players, Entities, Worlds and Blocks. These things are all members of Metadatable class(check [2])It works very simply. Everything that is Metadatable holds its own HashMap of Metadata which you have access to. That means, for example, if you're creating an economy plugin, you would need a HashMap of Player and Float or Double. With Metadata, you don't have to! You just attach to player new metadata value, and that's it!

Why to use Metadata

  • Metadata is all handled by Bukkit, which makes it a very good alternative to HashMaps.
  • Metadata can be used to share information between plugins.

Why not use Metadata

  • Slightly more difficult to get the value.
  • It is not saved on shutdown (but then again, neither are any Maps that you create).

Getting & Setting Metadata

public void setMetadata(Metadatable object, String key, Object value, Plugin plugin) {
  object.setMetadata(key, new FixedMetadataValue(plugin,value));
}

public Object getMetadata(Metadatable object, String key, Plugin plugin) {
  List<MetadataValue> values = object.getMetadata(key);  
  for (MetadataValue value : values) {
     // Plugins are singleton objects, so using == is safe here
     if (value.getOwningPlugin() == plugin) {
        return value.value();
     }
  }
  return null;
}

Note: If you're manipulating with numbers, booleans or strings, use convenient method to get the result. For example, you can use asInt(), asString() or asBoolean() instead of value to find out the value.

Databases

Sometimes flat files aren't enough for what your looking to do, this is where databases come in. The most common database engines available on Linux/Mac/Windows machines typically run on some flavor of SQL (Structured Query Language).

Software offering SQL allow you to create databases with columns and header to identify to contents of each cell. Think of it as a spreadsheet on steroids, where every column you set up in your database can enforce rules to ensure integrity. Apart from being more organised than a simple custom data file, SQL provides faster access and better searching than flat files.

The SQL standard helps applications like Bukkit implement database storage for their data in a consistent way. Unfortunately, there's more than one SQL-ready database engine, and each has minor differences in how to configure and use it. Which one you choose may depend on your particular needs. (Some plugins even offer configurable options to connect to multiple database engines!)

SQLite

Alta189 has written a fantastic SQLite tutorial which I suggest you watch if you're interested in using SQL in your plugins, included with the tutorials is a handy library you can download and import to make using SQL easier. Once you have watched these video tutorials I would suggest you go and learn some SQL syntax, it's very straightforward and shouldn't take you long to pick up. SQL Tutorials @W3Schools and @1Keydata.

SQLite is great for very simple databases, because there's no server concerns to set up. Just make a few calls to create a new database and table. It's easy to back up: just copy the whole database file in one go. SQLite is a little bit weaker at data integrity, flexibility in data types, and it may not be something you would want to trust for huge databases of millions of rows. But for a new plugin in development, it's often easiest and fastest to get the SQL basics squared away with SQLite, even if you "graduate" to a more server-class database engine later.

MySQL

Another popular SQL database engine is called MySQL. It is closer to server-grade than SQLite, where many popular companies or websites depend on it for millions of webpage hits every day. With that security comes a little bit steeper learning-curve, because MySQL has more tunable parameters and capabilities.

The coding for plugins accessing MySQL is mostly the same as tiny SQLite or mega-sized Oracle, with only small differences in syntax here or there. But the administration has room to grow. You may want to set up accounts and privileges inside your MySQL setup. You may want to set up SQL scripts that organize your backups and rollback to previous states.

部署你的插件

总之你写好的你的插件,如何把它变成一个可安装在服务端的jar文件呢?

首先我们需要建立一个水桶服务器。 在这里 能找到开服相关信息.接下来你需要把插件导出为一个.jar文件,

这样你就能在你的服务器里运行它。 在Eclipse里, 右键工程,点击Run as > Maven install即可:

Maveninstall.png

将来呢, 当你对插件进行了改动后,在做上述操作之前,如果你想删除之前的.jar文件,你可以右键工程,点击 Run as > Maven clean 。 

如果你在编译插件时遇到了问题,  请检查你的JDK是否正确安装, 并浏览建立工作区条目。

如果你在Eclipse控制台看到了和JDK有关的错误,你可能需要手动调整JDK,因为Eclipse的检测可能发生了错误。 

点击Window -> Preferences, 然后是Java -> Installed JREs。选中你最近安装的JDK作为Java运行环境(JRE)并添加, 并取消掉原来那个报错的JDK:

Jrelocation.png

如果你的工程成功被编译,JAR文件会生产在 target 目录下,具体在Eclipse的工作空间里名字为工程名的文件夹中。

这个JAR文件就是一个能正常工作的Bukkit插件了。当然前提是你的plugin.yml里没出差错。

你可以把这个jar文件丢到服务器的plugins文件夹里,重载或重启服务器,就可以测试你的新插件啦。

为了连接上使用你自己的电脑开的服务器 ,在多人游戏的ip里填上你的本地ip即可。

如果出现了错误而你又不能解决的话, 试试这里plugin development forum, 

在这里提问bukkitdev IRC channel,或者重新阅读wiki。

当你做完了一个实用的插件,可以考虑把它发表在这里dev.bukkit(为了Bukkit社区的访问量)。

以上可知,JAR格式是默认压缩生成的(JAR文件基于ZIP文件的格式)。这么说来,在上传插件到Bukkit上时把JAR文件压缩成ZIP只会增加文件体积罢了。

而且呢, 放在JAR中的config文件可以在检测不到插件目录(插件自动生成的文件夹,名字与插件名相同,里面是该插件的配置)下的config文件时自动生成。

所以我们一般不把jar文件转换成其他格式。

Importing other plugins

You may wish to edit another plugin that has the source available. If that plugin has a pom.xml in its folder (most of the popular ones, for example WorldEdit and Essentials, do), you can import it as a project by selecting File -> Import, and then opening the Maven folder and selecting Existing Maven Projects:

Importmaven.png

Then select the folder that the pom.xml is in, and the project should be on your sidebar. Edit it and compile it like you usually would.

小贴士

使玩家着火

Bukkit API有很多碉堡的功能。以下是实现一些功能的代码片段。 

下面的代码运行一个玩家使另一个玩家着火。通过类似于/ignite Notch这样的指令,这个指令会让 Notch 着火。

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
    // 使用 equalsIgnoreCase() 来忽视大小写
    if (cmd.getName().equalsIgnoreCase("ignite")) {
        // 确保玩家只输入了一个自变量(要被点着的玩家名).
        if (args.length != 1) {
            // 当return false时,该指令的使用信息会显示出来。
            return false;
        }

        // 确保命令发出者是个玩家.
        if (!(sender instanceof Player)) {
            sender.sendMessage("Only players can set other players on fire.");
            sender.sendMessage("This is an arbitrary requirement for demonstration purposes only.");
            return true;
        }

        // 获取那个要被点着的玩家. 请注意下标起始是0,不是1.
        Player target = Bukkit.getServer().getPlayer(args[0]);

        // 确保这个玩家在线。
        if (target == null) {
            sender.sendMessage(args[0] + " is not currently online.");
            return true;
        }

        // 让这个玩家着火1000 ticks (一秒钟大概有20ticks,所以总共是50秒).
        target.setFireTicks(1000);
        return true;
    }
    return false;
}

干掉玩家

和标题一样.这里是要干掉某个玩家。 

在你的onCommand方法里使用这个:

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
    if (cmd.getName().equalsIgnoreCase("KillPlayer")) {
        Player target = sender.getServer().getPlayer(args[0]);
         // 确保玩家在线.
        if (target == null) {
            sender.sendMessage(args[0] + " is not currently online.");
            return true;
        }
        target.setHealth(0); 
    }
    return false;
}

这里有个小扩展,会使用一个爆炸来杀死玩家:

float explosionPower = 4F; //爆炸等级 - TNT的爆炸等级默认是4F
Player target = sender.getWorld().getPlayer(args[0]);
target.getWorld().createExplosion(target.getLocation(), explosionPower);
target.setHealth(0);

创造一个假爆炸

这段代码会生产一个看起来和听起来与TNT/苦力怕产生的爆炸一样的效果。但是不会对实体和方块产生破坏。 在保留爆炸的艺术的同时又削弱了爆炸的威力,真是太有用了(

@EventHandler
public void onExplosionPrime(ExplosionPrimeEvent event) {	
    Entity entity = event.getEntity();

    // 如果事件和点燃的TNT有关,执行一些代码(又略)
    if (entity instanceof TNTPrimed) {
        entity.getWorld().createExplosion(entity.getLocation(), 0);
    }
}

使一个玩家对另外一个玩家隐形

通过输入指令,能让指令执行者对特定的玩家隐形。 然而其他人仍然能看到指令执行者。
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
    if (cmd.getName().equalsIgnoreCase("HideMe") && args.length == 1) {
        if (!(sender instanceof Player)) {
            sender.sendMessage("Only players can use this command!");
            return true;
        }
        // 检测指令执行者是玩家后,我们便可以安全的进行强制转换类操作。
       Player s = (Player) sender;

       // 获取命令执行者指定的玩家
       Player target = Bukkit.getServer().getPlayer(args[0]);
       if (target == null) {
           sender.sendMessage("Player " + args[0] + " is not online.");
           return true;
       }
       // 将 s 从 target 的视野中隐藏起来.
       target.hidePlayer(s);
       return true;
   }
   return false;
}

在玩家准星所指处生产闪电

如下代码运行手持鱼竿的玩家通过点击来生成闪电 (当然要用准星瞄准)。 真是个简单好玩的恶作剧(。

@EventHandler
public void onPlayerInteractBlock(PlayerInteractEvent event) {
    Player player = event.getPlayer();
    if (player.getItemInHand().getType() == Material.FISHING_ROD) {
        // 在给定坐标中生成一道闪电. 在本例中, 这个坐标是玩家准星瞄准的地方.
        // 只能指向200格以内的坐标.
        player.getWorld().strikeLightning(player.getTargetBlock(null, 200).getLocation());
    }
}

代码自动格式

Eclipse提供了将你的代码自动转成Oracle规定的格式, 修正了不符合常规的缩进, 空格等等。 在侧边栏选中你的项目,然后选择Source -> Format即可。

插件需求专区

http://forums.bukkit.org/forums/plugin-requests.13/

范例文件和样板


If you have any more questions on this matter, don't hesitate to visit the BukkitDev IRC channel and ask!