Minecraft(我的世界)中文论坛

 找回密码
 注册(register)
查看: 13340|回复: 48

[插件开发教程] 某wyt的Bukkit插件开发教程[主体&答疑]

[复制链接]
发表于 2016-8-3 09:39:49 | 显示全部楼层 |阅读模式

您尚未登录,立即登录享受更好的浏览体验!

您需要 登录 才可以下载或查看,没有帐号?注册(register)

x
本帖最后由 914554688wyt 于 2016-8-20 12:18 编辑

0.序章
*什么是Bukkit

[以下为本人口胡时间]
从前minecraft官方推出了一款服务端,这里暂且成为官服.
但这个官服呢,又和mc的尿性一样,没有开源(混淆了),而且对外兼容性差! (不能兼容mod,插件那时还没有)
于是呢,就有一个团队,他们把官服给强势反混淆(人工的)了一遍,然后在其中强力插入♂某些需要的内容,然后封装做成现在的bukkitAPI供我们调用. 于是就有了现在的插件....
虽说反混淆了,但是仍有许多方法名是没改过来的,所以如果你去反编译一下net.minecraft.server包的话,大概看到的方法名都是a,b,c,d......
[以下为正文]
Bukkit About Us

读前准备
本教程不考虑没有java基础的初学者。
换句话说,本教程要求所有的阅读者都掌握Java语法且清晰的了解以下概念

[题外话:我就是半吊子从其他语言边写插件边转过来的,然后现在还得恶补一番.]
标有*的为非必须,但推荐阅读相关资料.
粗体的为必须中的必须,必须掌握并熟练运用

x.png
还需要什么
对mc与编程的热情.(必须): 经常需要看一大坨别人的代码,如果你没有这个耐心与逻辑思维,学学倒是可以..
坚强的,耐挫的决心. : 论如何调试代码? 都说是试... 很多时候你没法直接定位错误在哪,这个时候该怎么办?.....
对mc游戏内容的了解 : 这个不解释
JDK与一个IDE: JDK7或者JDK8, IDE推荐EclipseIntellijIDEA
一个bukkit的JavaDoc : Bukkit 1.10.2-R0.1-SNAPSHOT API

若有写的不好的地方,欢迎各位斧正.


如果你已经阅读完毕这些东西并认为自己可以继续阅读,那你可以往下翻页了!







评分

参与人数 15人气 +26 金粒 +188 收起 理由
qaz2296777814 + 1 神乎其技,不服不行!
dust_of_heart + 2 + 35 这波教程很有用
1582952890 + 2 MCBBS有你更精彩~
SYS_TEM + 2 MCBBS有你更精彩~
henry5041 + 2 + 25 MCBBS有你更精彩~
灬友情灬 + 2 楼主你太帅了~
纯白剑姬 + 1 + 15 为写教程的大触献上膝盖
hsk001ufo + 2 + 28 MCBBS有你更精彩~
PETER·篠 + 3 + 25 MCBBS有你更精彩~
蓝色的鲨鱼 + 1 + 5 很棒的作品!
_Benson_ + 3 MCBBS有你更精彩~
RecursiveG + 1 + 30 已加入教程全家桶
吕乐乐 + 1 + 10 楼主你太帅了~
SpaceBang + 1 + 15 很棒的作品!
XJue_DaDa + 2 Mcbbs有你更精彩~

查看全部评分

 楼主| 发表于 2016-8-3 16:44:30 | 显示全部楼层
本帖最后由 914554688wyt 于 2016-8-25 17:26 编辑

1.创建一个空白插件

安装好jdk和IDE之后,我们就可以准备写第一个插件了.
[文中非关键步骤我都只是一笔带过,如果看不明白可以去baidu,Google,或者找别人的教程看看有没有说明.
总是依赖别人替你做好所有事情永远也不是一个好码农]


1.首先新建java项目, "MyFirstPlugin".
2.右键项目,属性,将编码设置为UTF-8, 并在"java构建路径"中将服务端的jar文件添加到引用的库.
QQ截图20160803162335.png

3.
右键项目,新建plugin.yml文件,按如下格式
QQ截图20160803164356.png
plugin的g漏掉了.... 不要在意
这个文件用于让bukkit识别这是一个插件.bukkit加载插件最先会读取这个文件,获取主类路径,然后再一路加载下去

需要遵循Yaml语法.
name - 插件名称 (推荐全字母)
main - 一个类的完整类名,这个类,需要继承Bukkit中的JavaPlugin类,又称插件的主类
version - 能表示插件版本的字符串.

特别容易错的地方有: 冒号后面没加空格, 主类路径打错, 作死用中文 值可以用
错误plugin.yml在开服时会有一段报错并导致插件不加载,就像这样: Invaild plugin.yml.

4.
打开项目,在源文件夹(\src)下,新建包com.github.MrWub.MyFirstPlugin, 在这个包下新建类MyFirstPlugin.

那么我们就说MyFirstPlugin类的完整类名为com.github.MrWub.MyFirstPlugin.MyFirstPlugin
plugin.yml的main项必须与该类完整类名一致,就像我所写的一样.

5.
使得主类继承bukkit中的JavaPlugin类

x.png
这时候出现了编译错误,有可能是因为JavaPlugin类未导入. 可使用IDE的快速补全解决.
QQ截图20160803163703.png
这类问题以后非特殊情况不再特殊说明.

6.重写onEnable()与onDisable()方法

x.png
Bukkit会在启用某插件时调用 (继承JavaPlugin类的) 主类的onEnable()方法,禁用某插件时调用onDisable()方法.
**小括号内为注释与补充,可以忽略**
**中括号内为题外话**
重写这两个方法,这样我们就可以让他在启用禁用时做我们想做♂的事情.
其实还有onLoad()方法,这个方法仅在开服加载插件时调用一次.
(开服时,插件会被加载并启用,
reload时,插件会先被禁用,再被启用,
关服时,插件会被禁用.)

[推荐大家去使用反编译工具(例如jdgui)看一下JavaPlugin类的源码,这样印象会深很多.
其实不止是javaplugin的,其他很多常用类都可以看看...]


7.在启用时向控制台输出信息
: 这不很简单吗,在onEnable()中添加System.out.println()?
不不不 我们有更高级的方法 this.getLogger().info("msg"); 这是一个定义在javaplugin类中的方法.
这个方法能够记录一条日志信息并在控制台即时显示.

不过,这玩意不支持颜色代码.这个之后会讲.
8.完工!
确认你的插件没有编译错误,确认plugin.yml编写正确后,选择项目,导出jar文件(不是可运行的!)
将得到的jar文件放到服务端的plugins文件夹中,运行服务器吧!









评分

参与人数 1金粒 +1 收起 理由
星燚 + 1 建议getServer().getConsoleSender().sendM.

查看全部评分

回复

使用道具 举报

发表于 2016-8-3 16:45:05 | 显示全部楼层
?????
excuse me????
nms得到的途径可不是正规途径得到的
后来Bukkit被dmca你以为为啥呢-.-

评分

参与人数 1金粒 +1 收起 理由
gooding300 + 1 DMCA的原因我没记错是bukkit团队里一个人为.

查看全部评分

回复

使用道具 举报

 楼主| 发表于 2016-8-4 16:41:41 | 显示全部楼层
本帖最后由 754503921 于 2018-8-11 09:38 编辑

2.创建命令
论如何创建一个安全,优雅的命令
1.无报错,正常运行.
2.必须考虑权限问题.
3.使用简洁,参数顺序符合逻辑思维
创建命令的步骤如下.


1.在plugin.yml中注册命令,按如下格式


如何默认给玩家权限.



2.重写onCommand()方法
执行命令时会被bukkit调用的方法.
[其实整个框架你可以理解为有一层平面,然后从这个平面上垂下来一条绳子,你可以在绳子上挂东西,当上层需要的时候就把绳子收上去拿走东西... ]
可选择直接在主类中重写,也可以选择开一个命令执行类MyExecutor extends CommandExecutor,
然后在主类中getCommand("myhome").setExecutor(new 执行类()); 在这个类中重写onCommand();

onCommand()的形式是
public boolean onCommand(CommandSender sender, Command cmd, String label,String[] args) {
  /* 其中sender为命令发送者,   必定是Player实例或ConsoleSender实例.
       cmd Command实例,暂时无用.
       label 相当于主命令去掉/ ,如输入/gamemode 1,则label参数就为"gamemode".
       args 命令参数数组,如输入/gamemode 1 test 则args={"1","test"}.
  */
  return false; //返回值为false时,会提示玩家plugin.yml中usage的内容.
}

3.在onCommand()中填写内容
假设我们现在要新建一个命令,当他运行的时候给玩家发送一行聊天框信息,这个命令只允许玩家执行
QQ截图20160804165817.png

5.导出运行试试吧!
未完待续
强烈建议论坛支持Markdown语法!
版主留:你的 command 少了个 s,已编辑






评分

参与人数 1人气 +1 收起 理由
太帅是罪过 + 1 plugin.yml文件cmd 那条少了个s

查看全部评分

回复

使用道具 举报

 楼主| 发表于 2016-8-6 08:01:37 | 显示全部楼层
吕乐乐 发表于 2016-8-5 20:54
可是我我并不会怎么将在NetBeans IDE 7.2里的插件导出?
变成JAR?

如果我记得没错的话是构建,然后在build文件夹下有.
回复

使用道具 举报

 楼主| 发表于 2016-8-6 08:18:39 | 显示全部楼层
吕乐乐 发表于 2016-8-6 08:14
谢谢了,不过我发现构建之后.JAVA都构建好了编程class
但是plugin.yml等文件却没有构建,如何修复呢。
以 ...

....netbeans并没有用过... 但肯定有办法的
再不行就手动拖进去呗 \*_*/
回复

使用道具 举报

 楼主| 发表于 2016-8-6 08:50:36 | 显示全部楼层
本帖最后由 914554688wyt 于 2016-8-6 20:00 编辑

3.配置文件(YAML)
bukkit自带的是snake_yaml库.
不过我们并不需要直接用,Bukkit都给我们封装好了.
1.在plugin.yml旁边新建config.yml, 并在其中写入默认配置(遵循yaml语法)
2.了解如下有关的方法
QQ截图20160806085343.png

实现ConfigurationSerializable接口的类可以作为set方法的第二个参数
因为实现了该接口的类允许被序列化/反序列化到配置文件或BukkitObjectOutputStream/BukkitObjectInputStream中
如果你说,我只添加了 implements ConfigurationSerializable,需要实现的两个方法我摆个空的在那里,那可不可以做到序列化/反序列化呢?
你说呢?....对于这类问题我不想解答.

解释一下"路径":



3.然后....好像就没有了...这些方法已经足以让我们进行配置文件的读写了.
当然yaml的神奇用法远远不止这些,可以去snake_yaml库里看看.

稍微补充一下yaml的基本语法
键:[空格]值
值如果是字符串则需要用'字符串'这样的形式括起来,当然这是在你手动修改的时候(虽然不括也行,但是括起来更规范)
不允许使用tab,每一层缩进为两个空格

list的形式是
key:
  - xxx
  - xxx
....

回复

使用道具 举报

 楼主| 发表于 2016-8-6 15:15:01 | 显示全部楼层
本帖最后由 914554688wyt 于 2016-8-6 15:18 编辑

4.监听事件
事件是什么
顾名思义,一件事,如: 玩家传送,实体死亡,交互等等等等..
bukkit在NMS中插入了许多不可描述的代码,成功的做成了一套事件触发系统.

监听器是什么
一个方法,bukkit检测到 事件类型与该方法参数类型符合时 会调用
(bukkit会根据注解来获取监听器,根据参数来判断是哪种监听器.)


即:
NMS运作 -> 不可描述的代码通知事件系统发生事件 (又名唤起) -> 事件系统调用监听器.


1.
首先在事件系统中注册监听器,这一步一般在启用时执行
Bukkit.getPluginManager().registerEvents(Listener实例,JavaPlugin实例);
Listener实例 - 监听器必须在一个实现了Listener接口的类中,但实现这个接口其实和没实现没区别...
JavaPlugin实例 - 你的主类.
比如说,我要在主类中创建监听器
QQ截图20160806144924.png

又比如说,我觉得在主类中太乱了,我想新开一个类放监听器
QQ截图20160806145055.png

没有本质上的区别,仅仅是监听器挪了位


2.
创建监听器,为了让Bukkit在Listener实例中找到你的监听器,这个方法需要添加@EventHandle注解
(他是怎么找到监听器的? 反射,详情请见java.lang.reflect包)
[一个记录着java自带类与方法如何使用的官方规范是必要的,就像这个,JAVA1.6API规范(中文).
更高版本的并没有中文,汉化团队强行弃了一波坑,Eclipse就自带英文的]

就像这样
xx.png

这个方法内可以做任何你想做的事情

更高级的玩法
EventHandler可以带参数,就像这样
@EventHandler(priority = EventPriority.LOW)
这个代表他的优先级.
若有不同监听器监听同一个事件(这些监听器有可能在别的插件中!)那他们的调用顺序由优先级决定
由低到高执行.
  • EventPriority.LOWEST (最先)
  • EventPriority.LOW
  • EventPriority.NORMAL
  • EventPriority.HIGH
  • EventPriority.HIGHEST
  • EventPriority.MONITOR (最后)

一个良好的插件应该具有良好的兼容性.
请按照你的需要调整优先级,比如你是一个反作弊插件需要监听玩家执行命令,那么你的优先级应该是lowest,即为最先捕捉命令并拦截.
请不要在优先级为MONITOR的方法中修改事件结果,那就无法保证别的插件能监听到你修改后的结果.
反正就最好留下余地,不要把事情做死.[这算是职业道德么233]
[就和你每个方法都final一样恶心]

Cancellable接口
一个实现了Cancellable接口的事件则可以被取消,何为被取消?
比如一个玩家打羊羊君
我把这个事件取消了,那就不会有任何反应,反之羊会受到伤害并被击飞
大部分Bukkit自带事件都实现了这个接口,我们可以通过这两个方法来判断他们是否被取消.
setCancelled(boolean)
isCancelled()
被取消的事件依旧会继续调用接下来的监听器,所以在你捕捉到事件之后先需要判断他是否被取消[虽然总是忘记]

事件中通常会携带这个事件的有关信息,若想得到这些信息则需要到bukkit的javadoc中去找相关的方法.



评分

参与人数 1人气 +1 金粒 +15 收起 理由
SpaceBang + 1 + 15 可以这很详细

查看全部评分

回复

使用道具 举报

 楼主| 发表于 2016-8-6 22:26:52 | 显示全部楼层
本帖最后由 914554688wyt 于 2016-8-7 15:11 编辑

5.物品(ItemStack)类与背包(Inventory)类
先说ItemStack类(Material或ID + 数量 + damage(附加值,比如不同颜色的羊毛有着同样的ID不同的附加值) + 其他数据).
他代表着游戏中的一格物品,注意是一格而不是一个.即他有"数量"这个属性.
ItemStack的构造方法与方法javadoc里有很清晰的解释,这里就不多说了,说了也是误导大家.

值得一提的是ItemMeta这个类.
他储存了物品的许多的属性如显示名称(物品的名称与显示名称是不一样的),附魔,lore(就是悬停时那一小行说明)等等
获取与修改的方法也在ItemStack的javadoc中,需要注意的是 他是传出一个clone的对象,而不是直接传引用的,所以需要重新set回去.
这些其实都是储存在NBT中的数据,只是部分被转换成了ItemMeta而已.


Inventory (WIKI) 类描述了一个背包
种类也有许多 QQ截图20160806220736.png
最常见的就非PlayerInventory(玩家背包, 即包括装备栏)莫属了.
如何创建一个背包
Inventory类并不能实例化,需要用到bukkit类的一个静态方法,自行查阅.
Bukkit.createInventory(..)

对一个背包,我们有如下几个关键方法
addItem(ItemStack) - 给这个背包添加一个物品,返回值为一个HashMap,塞不下的物品会被返回.
contains(ItemStack) - 该背包中是否包含给定的ItemStack
getItem(int) - 根据下标获取相应的物品
getTitle() - 得到左上角所显示的标题.
等等......  
[对应的下标的获取方法下面有]

假如我们现在创建好了一个背包,也给里面添加好了物品.
如何让一个玩家看到呢?
Player类的openInventory(Inventory)方法.

在该玩家的界面打开背包. (如果背包类型是默认的箱子背包的话, 玩家看到的就和打开一个箱子所显示出的界面一样.)

那玩家会不会拿里面的东西? 那可都是我辛辛苦苦创建出来的宝♂贝QAQ [大雾]
所以说,我们要监听玩家拿取东西的事件并取消掉他.
这时我们就需要监听事件InventoryClickEvent.
这个事件会触发在玩家点击一个Slot(物品槽)的时候,即使这个物品槽是空的.
只要我们确定这个背包是我们自定义的背包,就可以取消这个事件使得玩家无法点击物品,达到无法拿出的效果.
如何确定呢? 没错,就是靠判断背包的title.
如何查看下标? InventoryClickEvent的getSlot()方法,会返回物品槽下标.




[不知道为什么,看javadoc的英文看多了连中文语序都变了..可怕的文化侵蚀]


需要注意的地方
Assuming the EntityHuman associated with this event is an instance of a Player, manipulating the MaxStackSize or contents of an Inventory will require an Invocation of Player.updateInventory().

InventoryClickEvent的javadoc中有这么一段话,其大意为如果触发这个事件的实体是一个玩家(虽然我不知道非玩家如何触发这个事件),在修改他的背包之后需要调用Player.updateInventory()这个方法更新他的背包.





回复

使用道具 举报

发表于 2016-8-7 09:15:16 | 显示全部楼层
吕乐乐 发表于 2016-8-6 08:20
是的= =我采取的办法就是手动打开jar,
然后用手拖进去,可是总忘了把我写的config.yml拖进去,
于是插件 ...

???????,netbeans在对应的工程文件夹下面.dist/文件夹下面有构建好的jar
回复

使用道具 举报

 楼主| 发表于 2016-8-7 16:21:35 | 显示全部楼层
本帖最后由 914554688wyt 于 2016-8-19 19:48 编辑

6.关于"数据包".
本章内容在bukkit javadoc中无涉及,
需查看NMS包.
服务端与客户端之间的通讯,全部以数据包来往的方式进行.然而Bukkit并提供没有收发包的API,具体原因嘛...自己脑补一下我也不知道.

这里暂且不涉及到具体通讯方法(Socket,暂时不打算搞)
每一种数据包都被封装为一个类,这个类在net.minecraft.server.版本包中,类名格式为Packet + 类型 + (Out/In) +内容.
类型有四种
Login - 加入服务器时.(与登录插件无关)
Status - Ping与MOTD等
Play - 游戏内容
Handshake - 握手包


Out 是指 传出,即发送给客户端的包
In 是指 收到,即从客户端传出的包
如何不使用别的工具(ProtocolLib说的就是你!)发包
(需要一定的反射知识)
[以下是本人口胡时间]
首先我们想,给玩家发包,应该是在Player类下
然后我们又发现,Player是个抽象类,于是我们找到他的实现类CraftPlayer.
我们发现一个很有可能的成员 playerConnection,但这个成员在CraftPlayer.getHandle()下,
继续顺藤摸瓜,找到getHandle的返回类型EntityPlayer,打开PlayerConnection类,
然后就可以看到在PlayerConnection类下的sendPacket方法!
[以下为正文]


通过不懈的探索与寻找(其实就是强行查找),我们找到了一个发包(至少是名字像)方法

public void sendPacket(final Packet packet)
但在Player类中没有包含getHandle这个方法,只在他的实现类CraftPlayer中有.但所有Player实例实际上也是一个CraftPlayer实例
假设有Player p;

1.这时候就要用到我们的反射大法,首先获取Class<CraftPlayer>实例,然后再获取他的"getHandle"方法,再执行获取EntityPlayer实例
Object entityPlayer = p.getClass().getMethod("getHandle", new Class[0]).invoke(p, new Object[0]);


可能有人会说,可以强转CraftPlayer然后直接调用getHandle啊

.. 如果出现导入,那么服务端换了版本就会抛ClassNotFound异常,因为
NMS与CraftBukkit包居心险恶的每个版本的包名不一样

所以说, 你强转CraftPlayer类不需要导入吗?

2.再获取他的playerConnection 字段
Object playerConnection = craftPlayer.getClass().getField("playerConnection").get(craftPlayer);
3.最后获取他的sendPacket方法

playerConnection .getClass().getMethod("sendPacket", new Class[] { getNMSClass("Packet") })

这个getNMSClass,是因为NMS居心险恶的每个版本的包名不一样,而这个包名的格式是
net.minecraft.server.vX_X_RX
然后bukkit的实现类都在craftbukkit包里,这个包名的格式是
org.bukkit.craftbukkit.vX_X_RX

这个好办,我们随便获取一个nms或者cb Class,然后取他的包名然后再处理就可以了,就像这样
version = Bukkit.getServer().getClass().getPackage().getName()
                        .substring(Bukkit.getServer().getClass().getPackage().getName().lastIndexOf('.') + 1);
*这里的getServer()返回的是CraftServer实例

得到版本后,就直接Class.forName(net.minecraft.server.版本.类名)获取NMS类就可以了.

得到sendPacket方法后,我们还需要处理他的参数Packet.

通过反射获取我们需要的包的构造器, 假如我要发PacketPlayOutTitle包
Object packetTitle = getNmsClass("PacketPlayOutTitle").getConstructor(new Class[] {有序的构造器参数类型的Class实例})
.newInstance(new Object[] { 有序的构造器参数 });


现在来完成最后一步, 执行sendPacket方法
playerConnection .getClass().
  getMethod("sendPacket", new Class[] { getNMSClass("Packet") }).invoke(playerConnection , new Object[] { packetTitle });
大功告成,这样你就成功的给玩家发送了一个Packet.

另外,我们还可以使用ProtocolLib进行收发包,监听包的收发等等....
梨子的ProtocolLib教程:
[Ni]ProtocolLib怎么玩|使用ProtocolLib发包或收包|突破Bukkit限制|只有想不到系列
http://www.mcbbs.net/thread-568714-1-1.html

番外
NMS包是不是有混淆? 有些包的字段是不是看不懂?
你需要这个
http://wiki.vg/Protocol
完整的描述了mc数据包每个字段的意义.






回复

使用道具 举报

头像被屏蔽
发表于 2016-8-10 15:18:49 | 显示全部楼层
914554688wyt 发表于 2016-8-7 16:21
6.关于"数据包".
本章内容在bukkit javadoc中无涉及,
需查看NMS包.

反射大法2333
回复

使用道具 举报

发表于 2016-8-10 18:03:11 | 显示全部楼层
那个bukkit的javadoc链接 404了
回复

使用道具 举报

 楼主| 发表于 2016-8-10 18:43:49 | 显示全部楼层
hsk001ufo 发表于 2016-8-10 18:03
那个bukkit的javadoc链接 404了

https://hub.spigotmc.org/javadocs/spigot/,可以尝试一下spigotAPI,与BukkitAPI98%基本相同

评分

参与人数 1金粒 +1 收起 理由
hsk001ufo + 1 谢了=。=正好要的就是spigot的

查看全部评分

回复

使用道具 举报

发表于 2016-8-10 21:09:55 | 显示全部楼层
等等..容器(container)?不是集合(collection)吗?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册(register)

本版积分规则

Archiver|小黑屋|Mcbbs.net ( 京ICP备15023768号-1 ) | 手机版本

GMT+8, 2018-10-21 18:12 , Processed in 0.106356 second(s), 9 queries , Memcache On.

"Minecraft"以及"我的世界"为Mojang Synergies AB的商标。本站与Mojang以及微软公司没有从属关系。

© 2010-2017 我的世界中文论坛 版权所有。本站原创图文内容版权属于原创作者,未经许可不得转载。

快速回复 返回顶部 返回列表