├── .nojekyll ├── README.md ├── _sidebar.md ├── index.html └── unit ├── 1-1.md ├── 1-2.md ├── 1-3.md ├── 1-4.md ├── 1-5.md ├── 2-1.md ├── 2-2.md ├── 2-3.md ├── 2-4.md ├── 3-1.md ├── 3-10.md ├── 3-11.md ├── 3-2.md ├── 3-3.md ├── 3-4.md ├── 3-5.md ├── 3-6.md ├── 3-7-math.md ├── 3-7.md ├── 3-8.md ├── 3-9.md ├── 4-1.md ├── 4-2.md ├── 4-3.md ├── 4-4.md ├── 4-5.md ├── 5-1.md ├── 5-2.md ├── 5-3.md └── pics ├── 0-1-pic1.jpg ├── 1-1-pic1.jpg ├── 1-1-pic2.png ├── 1-4-pic1.jpg └── 1-4-pic2.jpg /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdiant/BukkitDevelopmentNote/53b48ae74908150737235566e81b39c0a57ae266/.nojekyll -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 感谢您的访问. 2 | 本站旨在为BukkitAPI开发者提供清晰的入门资料. 3 | 4 | ## 版权声明 5 | 6 | 本站内容作者为 tdiant 和为本项目贡献内容、提出意见的朋友. 部分文献资料若涉及转载或引用, 将会在相关内容处标记. 7 | 8 | 文档中代码均以Apache 2.0开源, 包括正文部分插入代码. 9 | 本作品采用[知识共享署名-非商业性使用-禁止演绎 3.0 中国大陆许可协议](http://creativecommons.org/licenses/by-nc-nd/3.0/cn/)进行许可。 10 | 11 | 若需转载任何内容, 请详细注明出处, 仅供学习交流使用, 谢绝商业化使用. 12 | 13 | 本站图片资源部分来自互联网,仅学习交流使用. 部分图片由tdiant绘制. 14 | 15 | ## 联系方式 16 | 17 | 您可以在GitHub仓库中发起issue. 18 | E-Mail: i@tdiant.net 19 | 不一定及时回复. 20 | -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | * [首页](README.md) 2 | * 第一部分 基本概念 3 | - [写在前面](unit/1-1.md) 4 | - [MC服务端介绍](unit/1-2.md) 5 | - [代码中的MC世界](unit/1-3.md) 6 | - [检索需要的信息](unit/1-4.md) 7 | - [服务端与客户端](unit/1-5.md) 8 | * 第二部分 基础内容 9 | - [最简单的插件](unit/2-1.md) 10 | - [事件的监听](unit/2-2.md) 11 | - [配置API](unit/2-3.md) 12 | - [命令执行器](unit/2-4.md) 13 | * 第三部分 进阶功能 14 | - [箱子GUI的实现](unit/3-1.md) 15 | - [自定义事件](unit/3-2.md) 16 | - [深入plugin.yml](unit/3-3.md) 17 | - [配置API的序列化及遍历](unit/3-4.md) 18 | - [多线程与多任务](unit/3-5.md) 19 | - [自定义合成表](unit/3-6.md) 20 | - [粒子效果和音效播放](unit/3-7.md) 21 | - [世界生成器](unit/3-8.md) 22 | - [Title、Bar与计分板显示](unit/3-9.md) 23 | - [经验与成就](unit/3-10.md) 24 | - [插件系统基本玩法](unit/3-11.md) 25 | * 第四部分 常用依赖 26 | - [Vault](unit/4-1.md) 27 | * 第五部分 底层部分 28 | - [认识NMS与OBC](unit/5-1.md) 29 | - [自定义发包](unit/5-2.md) 30 | - [NBT数据操作](unit/5-3.md) 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bukkit Development Note 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /unit/1-1.md: -------------------------------------------------------------------------------- 1 | # 写在前面 2 | 3 | 也许你已经有了雄心壮志, 准备开发一个插件出来了! 但是等一下. 开发插件**也需要一定的基础知识**! 4 | 本文在编写时默认你已经具有了下面所罗列的能力: 5 | 6 | **了解Minecraft** 7 | 8 | 我们假定你已经对Minecraft有充分了解. 9 | 例如, 我们认为类似"哪些方块是玩家不能破坏的", "某个物品使用后是什么效果"这种问题是您早已明白的内容, 不会过分提及. 10 | 11 | **Java基础** 12 | 13 | 插件开发对Java语言能力要求并不高, 通常情况下插件开发只会用到最基础的Java语法知识, 且开发环境搭建极其简单. 14 | 但是不会Java的新手绝对不会开发插件, 绝对弄不明白如何开发插件. 15 | 本教程不针对Java初学者或从未学习过Java的人. 在本教程中, 将会尽可能避免较为复杂的Java语法知识. 16 | 17 | 本教程认为会Java还应当具备一定的开发能力和使用相关工具等资源的能力, 例如JavaDoc如何使用, 你正在使用的IDE如何操作等问题, 本教程不会提及. 18 | 19 | > 但是本教程有0基础入门版本, 网址见下: 20 | > [https://alpha.tdiant.net](https://alpha.tdiant.net) 21 | 22 | **编程的思维** 23 | 24 | 编程的思维在实际编写一个项目当中尤为关键. 25 | 有Java基础并不够, 只知道语法, 不知道怎么写, 与不会Java没有什么区别. 26 | 27 | 在本教程中, 我们能做的只是告诉你“有什么”, 而无法解决你的插件“怎么写”的问题. 28 | 例如, 我们告诉你“事件”, 但是如何利用“事件”真正的写出来一个“登录插件”、“商店插件”等各种插件出来, 这需要你自己思考! 29 | 30 | 希望你在实际开发中能够“脑洞大开”, 想出别人想不到的内容, 想出能打本文作者脸的好办法、新思路! 31 | 32 | **不觉得尝试是件麻烦事** 33 | 34 | 插件开发离不开调试. 35 | 请你在提出问题之前、编写插件的过程中, 不要忘记不断调试, 这样你才能知道你的插件是否真的可以用, 别人说的不如自己试的, 自己想的不如实际干的. 36 | 37 | 38 | -------------------------------------------------------------------------------- /unit/1-2.md: -------------------------------------------------------------------------------- 1 | # MC的服务端介绍 2 | 3 | 如果你想开一个MC服务器, 你会发现有各式各样的服务端可以选择. 由于MC是一款社区驱动发展的游戏, 各种各样的玩家社区正在以非常快的速度开发以及维护各式各样的服务端. 4 | 那么, 它们都是什么? 它们本质上是怎样的? 本节大致上以时间顺序进行叙述来讲述这个问题. 5 | 6 | ## 官服 7 | 8 | 最开始, Minecraft这款游戏只有Java版本, 在每个版本(不考虑远古版本)发布时, 开发商Mojang都会同时发布其对应的ServerJar文件. 9 | 利用ServerJar文件可以开启一个MC服务器, 这种服务器俗称为官服. 10 | 11 | 官服启动后, 会在根目录下创建`server.properties`文件, 这是它的配置文件. 12 | 13 | 在配置文件里可以对服务器端口等内容进行设置. 在这里需要重点提及的是其中的一个设置项目`online-mode`. 14 | 这一项默认的设置为`true`. 开启后连入服务器的客户端必须是使用正版登录的方式才可以进入. 如果把这一项手动改为`false`, 那么我们俗称的盗版玩家也可以连入服务器. 15 | Mojang设计这样的一种设置是为了给无法连网(指互联网)的服务器提供便利. 16 | 如果有台电脑开启了官服, 这台电脑没法联网, 但是这台电脑连入了某个局域网内, 局域网中的小伙伴们就可以愉快的玩耍了. 如果没有这样的设置项目, 不联网没法进行正版验证, 那就没办法确定正版身份, 多人联机在这种场合下无法正常进行. 17 | 18 | 由此看来, Mojang之所以允许盗版玩家的出现, 其实是为了给不能正常联网的玩家提供一种“离线模式”, 而绝不是官方允许盗版理所应当地正常游玩, 官方服务端在开发时根本就没有对盗版玩家“特殊照顾”. 19 | 但社区开发者在开发后续服务端时, 在部分场合考虑到并对盗版玩家进行了某些特殊优化. 20 | 21 | > 如果你还是一名MOD开发者, 试想, 在MOD开发时难免要对MOD进行调试, 而大多数调试工作其实开启的游戏客户端都是“离线模式”. 22 | > Mojang这种举措其实还能极大地促进MC游戏社区的发展, 显然利大于弊. 23 | 24 | ## Bukkit服务端 25 | 26 | 官服有显而易见的问题, 那就是没有“插件系统”. 27 | 如果你是一个盗版服务器服主, 你根本没办法对玩家进行“身份识别”, 因为所有的玩家都可以把启动器中的玩家名修改成另一个玩家, 从而“代替”他连入服务器. 如果你想把服务器内的TNT禁止掉, 或者你有特殊需求, 就偏偏想让玩家没法把手里的钻石扔在地上, 正常情况下根本无法在官服实现这个功能. 28 | MC日渐完善的红石和命令方块功能根本无法满足社区玩家对服务器自定义的需求, 因而一种成熟的“插件系统”显得极为迫切. 29 | 30 | Bukkit因此出现. 31 | 32 | Bukkit服务端启动后会额外多创建一个配置文件`bukkit.yml`, 还有一个名为`plugins`的文件夹. 33 | 服主可以将插件jar文件放入`plugins`文件夹并重新加载服务器使之生效, 极为方便地安装一个插件. 开发者可以利用Bukkit官方给出的API开发一款插件, 丰富并拓展玩法. 34 | 35 | ## Spigot服务端 36 | 37 | SpigotMC社区发布了Spigot服务端. 最开始是对Bukkit服务端的各种优化和拓展. 38 | 39 | 后来由于Bukkit服务端直接使用了Mojang的代码, 由于法律原因, Bukkit被Mojang起诉并败诉, Bukkit服务端停止开发. 40 | 虽然Bukkit停止开发, 但是社区内Bukkit的热度依然很高, Bukkit服务端插件数量极大. 后来SpigotMC社区渐渐地接管了Bukkit的开发工作, 这意味着SpigotMC社区接管了BukkitAPI的维护工作. 41 | 42 | SpigotMC为了逃避直接使用Mojang的代码带来的法律问题, 在发布服务端时用了`BuildTool`.具体运作模式大致是: 服主若想下载Spigot服务端, 需要先下载BuildTool, 正确使用BuildTool后, BuildTool会先下载一个官服Jar文件, 再下载SpigotMC社区特制的fernflower反编译工具把官服Jar文件反编译得到官服源代码, 再下载Spigot服务端的源码(这部分不包含Mojang的源代码), 一切准备完毕后, BuildTool会在用户的计算机上自动完成代码的植入以及编译工作, 制作出最终的Spigot服务端Jar文件. 43 | 由于这一过程中, SpigotMC社区没有直接给用户提供官方源码二次修改成品, 因而规避了法律问题. 44 | 45 | 目前网上流传的Spigot服务端现成的Jar版本, 绝大多数都是基于BuildTool编译后的成品Jar文件. 实际上在SpigotMC社区官方网站上不提供这些成品Jar文件. 46 | 47 | ## 其他的各种服务端 48 | 49 | 后来又有许多服务端基于Spigot进行修改, 创造出了其他各种各样的服务端. 例如`Paper`服务端, 它是基于Spigot服务端的社区优化版本. 50 | 如果你运行了一个Paper服务端服务器, 你会发现服务端目录下会有`bukkit.yml`、`spigot.yml`、`paper.yml`这三个配置文件, 足以看出这背后的故事. 51 | 52 | 社区内还有单打独干的服务端. 例如在输掉官司后, 一些Bukkit服务端的开发者参与了一项几乎完全脱离官方服务端代码的服务端, 即`Sponge`服务端. 53 | 54 | 最早时还有`MCPC+`服务端. 其开发者将Spigot与Forge整合在一起, 使服务器既可以装MOD又可以装插件. 后来`MCPC+`改为了`Cauldron`, 又衍生出了`KCauldron`, 但这三款服务端都停在了1.7.10版本不再更新. 55 | 社区还出现了`CatServer`、`Uranium`、`Arclight`等服务端, 它们可以也可以实现开出插件+MOD服的功能. 56 | 57 | > 社区中部分服务端存在极大争议, 并因此发生过一些影响深远的故事. 58 | > 本教程功能仅为介绍, 如果没有纳入某些常用的服务端不能代表作者对这些服务端持有任何形式的不良想法, 服务端名称的排序没有先后, 不能认为某款服务端的名字排在前面即是作者对该款服务端存在某些偏向性指向或理解. 59 | 60 | ## 关于本教程 61 | 62 | 本教程正如其名, 是Bukkit插件开发教程. 63 | 这意味着理论上应当支持Bukkit服务端、Spigot服务端和各种衍生服务端. Sponge服务端由于使用的API体系与Bukkit完全不一样, 本教程不考虑Sponge服务端. 64 | 65 | BukkitAPI由于发展原因变动很多, 本教程尽可能提及. 这意味着本教程的内容肯定是不能支持全部版本的BukkitAPI版本. 但是问题不大, 变动虽然多但是不大, 思路一致, 如果你足够清晰BukkitAPI, 你应当可以在你需要开发的版本中找到你想要的API用法. 66 | -------------------------------------------------------------------------------- /unit/1-3.md: -------------------------------------------------------------------------------- 1 | # 代码中的MC世界 2 | 3 | 你一定是玩过MC的. 那你一定也能想象出来代码化的MC是什么样子. 4 | 5 | 相信你有Java的开发经验的话, 一定了解JavaDoc的使用. SpigotMC官方提供了JavaDoc. 6 | > 最新版本JavaDoc网址: [https://hub.spigotmc.org/javadocs/spigot/index.html?overview-summary.html](https://hub.spigotmc.org/javadocs/spigot/index.html?overview-summary.html) 7 | > 旧版本JavaDoc网址(1.7.10): [http://jd.bukkit.org/](http://jd.bukkit.org/) 8 | > 9 | > 国内有一群热爱开发的人做出了中文JavaDoc, 开发时可以用以参考. 10 | > 最新版本中文JavaDoc网址: [https://bukkit.windit.net/javadoc/](https://bukkit.windit.net/javadoc/) 11 | > 他们的GitHub地址: [https://github.com/BukkitAPI-Translation-Group/Chinese_BukkitAPI](https://github.com/BukkitAPI-Translation-Group/Chinese_BukkitAPI) 12 | 13 | 本章大致上按照JavaDoc上罗列的包的顺序来介绍BukkitAPI所提供的各个体系和系统. 14 | **本章仅仅是对各个系统做大概的描述. 后续会详细讲述各个部分的内容.** 15 | 16 | ## 世界、方块与Material 17 | 18 | 想也不用想, MC不可能把一整个世界都存到一个文件里去, 否则这样的一个文件该有多大! 19 | 事实上, 一个完整的世界分为了很多`Chunk`, 也就是区块. 每个区块都是X和Z为16*16的范围. 你可以认为一个世界由许多区块组成. 20 | 21 | 在开发时, 我们把每个世界都看成一个`World`对象, 每个区块都看成一个`Chunk`对象. 22 | 如果我们想操作某个世界内的某个方块, 实际开发时我们可以直接调用`World`对象的方法, 而不用先寻找到`Chunk`再操作. 23 | 每个方块都是一个`Block`对象. 24 | 25 | 在BukkitAPI中引入了“材质”(Material)的概念. 比如一个石头方块, 它的材质就是`STONE`, 如果一个物品的材质也是`STONE`, 那么这个物品就是玩家手上拿着的石头方块物品了. 26 | 你可以为一个方块设置材质, 就像这样: 27 | 28 | ```java 29 | Block b = 方块; 30 | b.setType(Material.STONE); 31 | ``` 32 | 33 | 想必你也能猜到了, 怎样在一个世界里“删除掉”一个方块: 34 | 35 | ```java 36 | b.setType(Material.AIR); //设置成空气就好了嘛 37 | ``` 38 | 39 | 方块与方块各不一样. 有些方块是带有特有属性和功能的. 比如告示牌上面有字. 事实上, `Block`类有许多子类, 每个告示牌方块无论是墙上的还是地上杵着的都是`Sign`对象. 比如你可以像这样修改告示牌上的字: 40 | 41 | ```java 42 | Block b = 你获取到的告示牌方块, 你可以用getType判断一下它的Material是不是告示牌; 43 | Sign s = (Sign)b; //直接强制转换 44 | s.setLine(0,"测试"); //这样就把第一行修改成了字符串“测试” 45 | ``` 46 | 47 | **BukkitAPI的包分类是清晰的, 所有的方块子类都在`org.bukkit.block`包内, 你完全可以利用JavaDoc, 找到所需的子类并查看用法. Material枚举量的具体内容也能查询到. 后续将不再赘述.** 48 | 49 | ## 生物与位置对象(Location) 50 | 51 | ### 生物 52 | 53 | 在MC中, 所有的生物, 例如一只羊, 乃至一个僵尸, 又或者是玩家, 都是生物, 他们都是Entity类型的对象. 54 | 这个概念还可以更加进一步的扩充, 一个被点燃的TNT, 实际上, 它也是一个实体(TNTPrimed对象). 55 | 56 | 当然, 与方块`Block`类类似, `Entity`拥有很多子类. 这其中最显眼的一个子类就是`Player`类了. 57 | `Player`类代表玩家, 每个**在线的玩家**都有一个`Player`对象. 其实也不难理解, 如果一个玩家下线了, 那么这个玩家对应的实体当然也会消失. 你也许会纳闷, 那如何去操作一个目前不在线的玩家的数据呢? 这是个比较复杂的问题, 需要分成多个情况来解决, 这里暂不赘述. 58 | 59 | ### Location对象 60 | 61 | 任何一个坐标都可由一个Location代表. 62 | 常见的实体对象是Entity的子类,故都提供了`getLocation`方法,返回的`Location`对象代表着它们的坐标位置. 63 | 值得一提的是,如果应用`getLocation`获取实体位置,那么获取的位置是它的脚. 例如`Player.getLocation()`所获取的是玩家的脚的位置. 64 | 对于这些实体对象,如果想修改他们所在的坐标位置,Bukkit没有提供`setLocation`方法,而是提供了`teleport`方法. 通过`teleport`方法可以传送某个实体. 65 | 66 | **BlockLocation** 67 | Location中提供了`getBlockLocation()`、`getBlockX()`、`getBlockY()`、`getBlockZ()`四个方法. 68 | 对于一个方块而言,其坐标的XYZ值均为整数,所以这些方法所获取的是此Location对应的最精确方块的坐标. 69 | 通俗的理解,可以认为获取的是将XYZ四舍五入后的坐标值. 70 | `getBlock()`获取的此Location对应的最精确的方块的`Block`对象. 71 | 72 | **坐标运算** 73 | Location提供`add`(加)、`subtract`(减)方法. 74 | 75 | **两点间距离** 76 | Location提供`distance`方法,参数为另一个Location,返回值为double,代表两点间距离. 77 | *Location还提供`distanceSquared`方法,代表两点间的方块距离,遵循四舍五入.* 78 | 79 | ## 物品 80 | 81 | 玩家手里拿着的东西叫物品. 物品的材质也叫`Material`. 82 | 特殊的是, 某些物品与其对应的方块`Material`不一致, 例如红石比较器. 83 | 红石比较器物品的种类是`Material.REDSTONE_COMPARATOR`, 而放置后的方块种类又分为`Material.REDSTONE_COMPARATOR_ON`(开启状态), `Material.REDSTONE_COMPARATOR_OFF`(关闭状态)两种, 红石比较器方块的种类不能用`Material.REDSTONE_COMPARATOR`来表示. 84 | 85 | `ItemStack`用于反应一种描述物品堆叠的方式. 86 | 一个`ItemStack`的实例, 囊括了物品的种类(其对应的`Material`)和数量等信息. 87 | 例如, 玩家手中拿着三个苹果. 玩家手中的这三个苹果, 实质上是一个`ItemStack`, 它包括了这三个苹果的种类(`Material.APPLE`)、数量(`3`)与其他的一些信息. 88 | 89 | 90 | ## 事件系统 91 | 92 | ### 事件的概念 93 | 94 | 事件是服务器里发生的事. 95 | 例如, 天气的变化, 玩家的移动. 玩家把树打掉, 又捡起了掉落地上的原木. 这些都是事件. 96 | 97 | 事件分为可控事件和不可控事件. 其最大区别在于能不能取消(也就是能不能setCancelled). 98 | 不难理解, 玩家如果退出服务器, 这不能被取消, 它是不可控事件. 玩家的移动可以被取消, 它是可控事件. 99 | 100 | ### 事件的作用 101 | 102 | 利用BukkitAPI, 你可以监听事件, 事件触发时执行某些代码. 103 | 例如, 你可以监听玩家登录服务器, 玩家登录服务器后你可以执行某些代码. 104 | 105 | 那么, 如果你想写登录插件, 你需要监听玩家登录服务器的事件. 106 | 玩家进入服务器以后, 记录存储起来他的用户名. 等待玩家输入指令进行登录, 登录完毕以后去掉他的用户名. 107 | 然后再监听其他的各种事件(比如监听方块破坏事件), 如果这些事件被触发, 判断是哪个玩家触发的, 看看玩家用户名有没有存储起来, 如果有, 那么他没有登录, 那就把这个事件取消掉. 108 | 109 | 通过这样的例子可以发现, 事件是一个插件最重要的组成部分! 110 | 111 | ### 监听器 112 | 113 | 上面我们提到可以实现事件触发时执行某些代码. 实现这个目的的方法就是写一个监听器. 114 | 监听器实质上是一个实现了`Listener`的类, 其中包含一些带有`@EventHandler`注解的方法. 115 | 116 | 一个监听器大致是这样: 117 | 118 | ```java 119 | public class DemoListener implements Listener { //这是你定义的监听器类, 实现了Listener接口 120 | @EventHandler 121 | public void onPlayerMove(PlayerMoveEvent e) { 122 | //比如我监听了玩家移动事件, 就应该这样创建一个这样的方法 123 | //带有Listener注解, 参数是一个PlayerMoveEvent类型的参数, 代表你要监听的是PlayerMoveEvent事件 124 | //一个方法只能监听一个事件 125 | } 126 | } 127 | ``` 128 | 129 | 监听器类创建完毕后, 还需要注册它才可以真正发挥作用. 130 | 131 | ## 命令系统 132 | 133 | ### 命令 134 | 135 | MC中的命令是一个字符串, 用来实现游戏内高级功能. 136 | 137 | 在MC客户端中, 玩家将在聊天框内输入命令. 138 | 当且仅当在“聊天”内, 命令与普通的聊天内容的区别在于其内容的第一个字符是一个斜杠`/`. 139 | 140 | ### 对命令的代码解析 141 | 142 | 例如玩家依次输入了这些命令: 143 | 144 | ``` 145 | /abc 146 | /abc test 1 147 | /def create info username 148 | ``` 149 | 150 | 依次分析, 第一种和第二种**并不是两种命令**, 区分不同的命令看紧跟斜杠的词是什么, 所以第一个和第二个本质上是同一个命令. 151 | 152 | 按照一个空格一个分隔的规律, 开头的一节为命令的名称, 第二个以及第二个以后的部分都是这条命令带的参数, 也就是输入这条命令的人想要传递的数据信息部分. 153 | 参数部分可以表示成一个String数组, 以第三个命令为例, 参数部分可以表示为: 154 | 155 | ``` 156 | args[0]: "create" 157 | args[1]: "info" 158 | args[2]: "username" 159 | ``` 160 | 161 | 假如你想实现一个命令, 玩家必须输入参数, 参数第一项是固定的几种答案, 你完全可以对args做些if判断来实现. 162 | 163 | ```java 164 | if(args.length==0){ 165 | //玩家没有输入参数 166 | } else { 167 | if(args[0].toLowerCase().equals("固定答案1")){ //玩家输入了 /命令名 固定答案1 格式的命令 168 | //你想实现的功能 169 | } else if(args[0].toLowerCase().equals("固定答案2")){ //玩家输入了 /命令名 固定答案2 格式的命令 170 | //你想实现的功能 171 | } else { 172 | //玩家没有输入固定的答案类型 173 | } 174 | } 175 | ``` 176 | 177 | ## 权限系统 178 | 179 | BukkitAPI提供了一套权限系统. 180 | 利用权限系统, 你可以实现限制有某个权限的玩家能输入某个指令、做某些事情等功能, 但没有这一权限的玩家却做不了. 181 | 182 | 权限一般是指一串字符串, 一般(最好遵守这样的格式)格式是`插件名.功能名.某一个项目的名称.xxx`构成, 需全为英文小写. 例如`testplugin.block.place`. 183 | `Player`类有`hasPermission`方法可以检查某个玩家是否有某个权限. 184 | 185 | ## 配置API 186 | 187 | ## 交流系统(Conversation API) 188 | 189 | 需要注意的是, 虽然有这样的API, 但这一API在实际开发中使用频率很低. 190 | 191 | 如果你使用过`QuickShop`插件, 你可能会对这一功能有印象: 192 | 玩家创建了一个箱子商店, 当另一个玩家点击箱子方块前面的告示牌时, 在聊天区域会显示出商品的详情和价格, 并提示你直接在聊天区输入一个数字代表购买物品, 发送这个数字就可以购买了. 193 | 如果你细细琢磨一下这一功能, 其实你可以把这一过程看成一种对话, 你和插件可以直接在聊天区内进行交流: 194 | > 插件: 你好, 你需要多少A商品? 195 | > 你: 2 196 | > 插件: 好的, 购买成功! 197 | 198 | 其实这就是一个`Conversation`了. 你可以把这样的一个对话过程做成一个`Prompt`(可以把这个理解成对话的模板), 然后在需要的时候依照这个`Prompt`生产一个`Conversation`, 并给一个玩家开始对话, 过程就像这样: 199 | 200 | ```java 201 | Player p = 玩家; 202 | //一些生成Conversation对象的代码之后 203 | Conversation c = 你根据Prompt生产出的Conversation对象; 204 | c.begin(); //开始对话 205 | ``` 206 | 207 | ## Inventory 208 | 209 | 对于玩家背包、箱子里存放的所有`ItemStack`对象, 我们可以认为他们都储存在了一个`Inventory`对象里. 210 | 也就是意味着, 一个箱子、一个玩家都对应他们专属的`Inventory`对象, 用来储存它们存放着的物品. 211 | 212 | ## 服务器底层 213 | 214 | 我们把原版服务端部分(全部都在`net.minecraft.server`包内)叫做`NMS`. 把Bukkit部分的底层实现部分(也就是CraftBukkit部分, 全部都在`org.bukkit.craftbukkit`包内)叫做`OBC`. 215 | 有时候我们需要手工发送某些数据包来达到某些目的, 这时需要对底层代码进行操作. 通常不常操作OBC. 216 | -------------------------------------------------------------------------------- /unit/1-4.md: -------------------------------------------------------------------------------- 1 | # 检索需要的信息 2 | 3 | 在实际开发中你肯定需要检索需要的信息. 4 | 笔者在各个QQ群里、论坛里最经常看到的问题莫过于一些新开发者问的诸如 "XXX应该监听那个事件?"、"怎么设置玩家昵称?"、"怎么设置方块的Material?" 的问题. 5 | 实际上, 这些问题会被人视作"劣质问题". **因为这些问题的答案就是一查就能查到**. 说心里话, **有问问题的时间, 不如自己翻一下JavaDoc自己找到答案**. 6 | 当然, 要是真的找不到答案, 还是应该问一下. 7 | 8 | 本教程不涉及JavaDoc的基础使用(如果需要, 请翻阅基础版教程), 仅作为指向提醒作用. 9 | 10 | ## Bukkit各部分以包划类, 类名即表示其作用 11 | 12 | > 假如你想问的是`玩家移动应该监听什么事件?`这个问题. 13 | 14 | 首先, 你想问的是监听什么事件, 请打开JavaDoc看一看BukkitAPI给出的所有的包. 15 | 显然, 根据名称即可判断出你应当从`org.bukkit.event`包内寻找才对, 因为其他包跟"事件"这二字一点关系也没有. 16 | 然后你会发现`org.bukkit.event`包还分为若干子包, 你同样可以按照名称判断出与玩家有关的事件应该都在`org.bukkit.event.player`包. 17 | 18 | 打开`org.bukkit.event.player`包, 你会发现与玩家有关的事件基本上都是按照`PlayerXxxxxEvent`的格式命名的. 仔细看一看便知道, `PlayerMoveEvent`事件的名称与我们想要的玩家移动这一功能即为一致, 它很有可能就是我们想要的. 19 | 20 | 打开它的详细介绍, 看看它的描述, 是 21 | 22 | ``` 23 | Holds information for player movement events 24 | ``` 25 | 26 | 所以, 这就是我们需要的玩家移动事件了. 27 | 28 |
29 | 30 | > 假如你想问的是`怎么获取玩家的飞行速度?` 31 | 32 | 这与玩家有关, 肯定要考虑获取玩家飞行速度的方法在`Player`类中. 我们打开`Player`类的详细介绍寻找答案. 33 | 检索这一类的方法, 根据名称和方法描述, 可以得知我们想找的方法是`getFlySpeed`方法. 34 | 35 | -------------------------------------------------------------------------------- /unit/1-5.md: -------------------------------------------------------------------------------- 1 | # 服务端与客户端 2 | 3 | ## 永远不能相信客户端 4 | 5 | 有人说, 从游戏行业诞生的那一刻, 也是游戏反作弊诞生的那一刻. 6 | 客户端给的数据不能轻易相信. 在开发插件时一定要注意, 如果你的插件要接收某个信息, 这个信息不是你自己指定的, 而是服主设置配置文件自己设定的或者是客户端发来的, 那你一定要仔细思考, 多加留意. 7 | 8 | 虽然不是服务器插件开发圈的事情, 但是这件事情为我们敲响了警钟. 9 | 2020年2月, MCBBS网站以及线下圈子内突然爆发一股针对Minecraft 1.12版本MOD漏洞的争议. 10 | [https://www.mcbbs.net/thread-968903-1-1.html](https://www.mcbbs.net/thread-968903-1-1.html) 11 | 12 | 早前在GitHub上, 一个来自俄罗斯的账户发布了一个作弊MOD. 13 | 这款作弊MOD的作者研究了各大主流MOD的协议包漏洞, 发现这些知名的大MOD的协议包或多或少在接收到玩家客户端数据时, **对玩家客户端发来的数据的合理性判定极度缺失, 甚至是根本没有**. 14 | (不恰当的比方)玩家客户端说自己有一百万个钻石, 服务端真的就认为玩家客户端有一百万个钻石. 因此, 这款作弊MOD针对主流的大MOD的这些协议包问题, 有针对性的对每个MOD都开发了作弊方案. 15 | 16 | 作弊MOD事件过后, VexView的作者这样回应这一问题(以下节选有删改): 17 | 18 | > “没错, VexView很幸运, 同样存在这个BUG, 而且存在于 1.7.10-1.14 全版本. 但是不用担心, 我们当初在设计的时候就已经考虑到这个问题. 同时我们在开发文档中也明确指出要保持谨慎. 如果附属插件能够正常处理, 应该不会出现这些问题的. 当然不排除某些附属插件逻辑就不会存在此类刷物品的漏洞. **这完全取决于附属开发者**.” 19 | 20 | 可见开发者对客户端的警惕意识有多么重要. 21 | 22 | 我们要编写的是服务端插件. 23 | 服务端插件根本上面向于这一服务器的玩家, 最直接的使用者是这一服务器的服主. 那么, 服主能不能正确配置配置文件? 玩家的数据信息真的正确吗? 24 | 也许你说, 你写的又不是反作弊插件, 不可能做到处处提防玩家. 诚然如此, 但是警惕意识绝对不能丢. 25 | 26 | **先虑忧患, 享于安乐.** 27 | 28 | ## 理清楚客户端和服务端的关系 29 | 30 | 如果你有经验你应该知道, 装上`Resdience`插件后, 创建一个领地, 关闭领地所有玩家的移动权限, 把一个玩家扔进去, 玩家在里面试图移动的话, 效果并不是完全动不了, 而是玩家仍然可以运动, 但是在一个间隔时间之后会被“弹回来”. 这是为什么? 31 | 32 | 为什么最终的效果不是"玩家一点都动不了"呢? 33 | 34 | 事实上, 我们无法在服务端取消玩家一点也不能移动. 客户端移动玩家时, 会在客户端显示出移动后的样子, 然后才会传递给服务器玩家移动的信号, 服务端收到客户端的信号后, 服务器才会做出响应. 35 | 36 | 也就是说, 客户端与服务端之间, **客户端往往都是"先斩后奏"的**. 客户端不管你服务端想干什么, 先那么显示出来再说. 因为毕竟玩家在服务器里完全动不了不是MC原版的设定之一. 37 | 38 | 39 | ## UUID和玩家名 40 | 41 | Minecraft在1.8版本的更新中隆重引入了UUID, 以便正版玩家修改自己的游戏ID且游戏内数据不变. 42 | 这意味着从此以后服务端区分玩家的判据不再是玩家的ID, 而是UUID. 43 | 44 | 正版服务器不必多说, 通过Mojang提供的API, 服务端可以得知一个进入服务器的玩家的UUID, 数据按照玩家的UUID存储即可. 45 | 但是离线服务器怎么办呢? 答案是根据玩家名计算. 46 | 47 | ```java 48 | //下面的代码出自OBC的CraftPlayerProfile类 49 | if (isOnlineMode) { 50 | profile = lookupName ? userCache.getProfile(name) : userCache.getProfileIfCached(name); 51 | } else { 52 | profile = new GameProfile(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)), name); 53 | } 54 | ``` 55 | 56 | 可见盗版玩家的UUID计算的是`OfflinePlayer:玩家名`对应的UUID. 57 | -------------------------------------------------------------------------------- /unit/2-1.md: -------------------------------------------------------------------------------- 1 | # 最简单的插件 2 | 3 | # Bukkit插件的本质 4 | 插件本质是一个基于BukkitAPI的Java应用. 一个插件必须要有 主类 和 `plugin.yml`文件. 5 | 6 | 例如下面是一个常见插件, 让我们找一下它的主类和`plugin.yml`文件. 7 | 8 | ![](https://i.loli.net/2020/07/11/32WYP4VthqJDLyB.jpg) 9 | 10 | # 简单的插件 11 | 在编写自己想做的插件之前, 不妨做一个简单的插件来了解一下Bukkit插件如何编写. 12 | 13 | 新建一个Java工程, 导入开服用的服务端jar文件到工程的Libraries中. 创建`tdiant.helloworld.HelloWorld`类作为插件的主类, 并继承`JavaPlugin`类. 14 | 在主类里覆写`onEnable`方法和`onDisable`方法. 完成后, 代码应该类似这样: 15 | 16 | ```java 17 | package tdiant.helloworld; 18 | 19 | import org.bukkit.plugin.java.JavaPlugin; 20 | 21 | public class HelloWorld extends JavaPlugin { 22 | @Override 23 | public void onEnable() { 24 | System.out.println("Hello World"); 25 | } 26 | 27 | @Override 28 | public void onDisable() { } 29 | } 30 | ``` 31 | 32 | Bukkit服务端会在插件被启用时调用`onEnable`方法, 被停用时调用`onDisable`方法. 33 | 34 | 创建`plugin.yml`文件. 打开plugin.yml文件并在其中输入如下信息: 35 | ```yml 36 | name: HelloWorld 37 | main: tdiant.helloworld.HelloWorld 38 | version: 1 39 | author: tdiant 40 | ``` 41 | 42 | > **特别注意: 如果你的插件是基于新版本API(1.13以及以上版本)编写的, 应当在plugin.yml中额外增加`api-version: 1.13`键值对.例如这样:** 43 | > ```yml 44 | > name: HelloWorld 45 | > main: tdiant.helloworld.HelloWorld 46 | > api-version: 1.13 47 | > version: 1 48 | > author: BakaRinya 49 | > ``` 50 | > **这会告诉Bukkit, 这个插件是基于新版API编写的.** 51 | > 若要兼容1.13及以上版本的同时兼容旧版本, 应特别注意各版本之间的 API 变化(譬如1.12进度系统取代了成就系统, 1.13的 Material 枚举发生了巨大变化). 52 | 53 | *注意: 主类的名称并不是固定的, 但是`plugin.yml`文件的名称是固定的.* 54 | 55 | 上面的plugin.yml文件逐行分析如下: 56 | 57 | | 键 | 意义 | 备注 | 58 | | :-: | :-: | :- | 59 | | name | 插件名 | 不允许带有中文和空格, 推荐只含有下划线、英文. | 60 | | main | 插件的完整主类名 | 例如我这里插件主类为tdiant.helloworld.HelloWorld, 此处则需填写tdiant.helloworld.HelloWorld. | 61 | | version | 插件版本 | 您可以填写一个合理的String内容, 而不一定必须为数字, 例如可填写v1.0.0 | 62 | | author | 作者 | - 63 | 64 | 65 | 可以发现, 当插件Jar被正常加载后, 会在控制台输出`Hello World`字符串, 这标志着我们的HelloWorld插件正常工作. 66 | 67 | ![](https://i.loli.net/2020/07/11/wBv1hfPEIdRigHC.png) 68 | 69 | # BukkitAPI中的Logger 70 | 71 | ## Logger 72 | *这里只是简要提及, 不详细介绍, 只需要知道有这件事即可.* 73 | 74 | BukkitAPI“修改”了我们常用的sout (即`System.out.println`), 将其“引入”了BukkitAPI提供的Logger. 75 | 只有通过Logger输出的文本信息才能记录在服务端生成的log文件中. 76 | 77 | 在BukkitAPI插件开发时, 我们通常不用sout输出想往后台输出给服主看的文本信息, 而应用Logger. 78 | 主类有`getLogger()`方法, 可以利用这个方法获得Logger. 79 | 例如这样: 80 | 81 | ```java 82 | public class HelloWorld extends JavaPlugin { 83 | @Override 84 | public void onEnable(){ 85 | this.getLogger().info("Hello World"); 86 | } 87 | 88 | @Override 89 | public void onDisable(){} 90 | } 91 | ``` 92 | 93 | 这样输出信息的方式与sout相比最主要的区别是, 如果你的插件`plugin.yml`里的名称为`Test`那么: 94 | 95 | ```java 96 | this.getLogger().info("测试"); 97 | System.out.println("测试"); 98 | ``` 99 | 100 | 输出的结果是 101 | 102 | ``` 103 | [23:33:33] [Server thread/INFO]: [Test] 测试 104 | [23:33:33] [Server thread/INFO]: 测试 105 | ``` 106 | 107 | 108 | 109 | ## ChatColor 110 | 在所有能发彩色文字的地方, 你可以直接使用双s (即`§`符号, Windows系统下按住键盘Alt键, 在数字键盘区域依次按下0167后松开Alt键即可输入该字符) + 对应颜色代码(可以在Minecraft Wiki上查到)代表颜色. 111 | 颜色是可以混用的: `§4比§c如§6这§2样`. 112 | 113 | 在开发中, 你不必这样, `ChatColor`可以替代. 114 | 115 | ```java 116 | p.sendMessage(ChatColor.RED+"你" + ChatColor.GREEN+"好"+ ChatColor.YELLOW + "!"); 117 | ``` 118 | 119 | 这样就可以发送一个 红色的“你”, 绿色的“好”, 黄色的感叹号 给玩家. 120 | 121 | 后面了解配置文件的操作后, 一些插件允许服主在设定一些提示语时用`&`符号代替`§`, 插件处理这样的文本信息时, 可以这样处理成带颜色的字符串: 122 | ```java 123 | String str = "&4哈&c哈&6哈....."; //待处理字符串 124 | p.sendMessage(str); //发给玩家的还是: &4哈&c哈&6哈..... 125 | String str_finish = ChatColor.translateAlternateColorCodes('&',str); //处理好的字符串 126 | p.sendMessage(str_finish); //发给玩家就是彩色的 127 | ``` 128 | 129 | 130 | > 提示:你可以使用 131 | > ```java 132 | > import static org.bukkit.ChatColor.*; 133 | > ``` 134 | > 来导入`ChatColor`中的所有枚举。接下来你就可以更方便地写颜色代码: 135 | > ```java 136 | > String str = RED + "/test help" + GREY + " - " + WHITE + "显示帮助菜单。"; 137 | > ``` 138 | -------------------------------------------------------------------------------- /unit/2-2.md: -------------------------------------------------------------------------------- 1 | # 事件的监听 2 | 3 | 事件是服务器里发生的事. 4 | 例如, 天气的变化, 玩家的移动. 玩家把树打掉, 又捡起了掉落地上的原木. 这些都是事件. 5 | 6 | 事件分为可控事件和不可控事件. 其最大区别在于能不能取消(*也就是能不能setCancelled*). 7 | 不难理解, 玩家如果退出服务器, 这不能被取消, 它是不可控事件. 玩家的移动可以被取消, 它是可控事件. 8 | 9 | BukkitAPI给了一些基本的服务器事件. 大多数情况下可以满足我们的需求. 10 | 本章以监听这些事件为例, 讲述事件的监听如何实现. 11 | 12 | 13 | ## 监听器(Listener) 14 | 15 | 监听器实质上是一个实现了`Listener`的类, 其中包含一些带有`@EventHandler`注解的方法. 16 | 当服务器某个事件触发后, 例如玩家移动事件, 服务器就会创建一个对应的`PlayerMoveEvent`对象, 如果你的插件有注册并正在监听该事件的监听器, 那么服务端会按照`@EventHandler`注解找到对应的方法并调用, 你的插件因而便可监听到玩家移动事件了. 17 | 18 | 我们以一个登录插件作为展开, 写一个“玩家不登录就不允许移动”的插件出来. 19 | 因为截止到现在还没有说怎么注册命令, 这里我们设定玩家“只要右键空气就可以登录”. 20 | *这里我们为了偷懒, 下面把主类直接实现`Listener`当做监听器用. 其实可以分开* 21 | 22 | ```java 23 | public class HelloWorld extends JavaPlugin implements Listener { 24 | private List playerNameList = new ArrayList(); //这是没登录玩家列表 25 | 26 | public void onEnable() { 27 | this.getLogger().info("Hello World!"); 28 | Bukkit.getPluginManager().registerEvents(this,this); //这里HelloWorld类是监听器, 将当前HelloWorld对象注册监听器 29 | } 30 | 31 | public void onDisable() {} 32 | 33 | /* 功能一:刚进入服务器的玩家都记录到“小本本”playerNameList上,他们是没登录的玩家 */ 34 | @EventHandler // 这个注解告诉Bukkit这个方法正在监听某个事件 35 | public void onPlayerJoin(PlayerJoinEvent e) { // 玩家登录服务器就会调用这个方法 36 | if(!playerNameList.contains(e.getPlayer().getName())) { // 先判断这个玩家的名是不是记过了 37 | playerNameList.add(e.getPlayer().getName()); // 玩家一登录就给他记上名, 代表他没登录 38 | } 39 | } 40 | 41 | /* 功能二:没登录的玩家不让移动 */ 42 | @EventHandler 43 | public void onPlayerMove(PlayerMoveEvent e) { //玩家移动时Bukkit就会调用这个方法 44 | if(playerNameList.contains(e.getPlayer().getName())) { 45 | e.setCancelled(true); //判断玩家是不是没登录, 是则取消事件 46 | } 47 | } 48 | 49 | /* 功能三:右击空气登录(本质就是从playerNameList把他删了) */ 50 | @EventHandler 51 | public void onPlayerInteract(PlayerInteractEvent e) { // 玩家交互时会调用这个方法(这个下面会解释) 52 | if(e.getAction()==Action.RIGHT_CLICK_AIR) { // 判断是不是右键空气 53 | playerNameList.remove(e.getPlayerName()); 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | 从上面的代码我们可以看出每一个事件都对应着一个`XXXEvent`对象. 事件类都以`Event`作为名称的结尾. 60 | 61 | **监听器类里由若干个带`@EventHandler`注解, 参数仅为一个`XXXEvent`的方法. 这些事件触发后会触发这些方法, 这就是事件监听的本质.** 62 | 要特别注意, **监听器中带有`@EventHandler`的方法一个只能监听某一个事件, 而不能监听多个事件!** 换而言之, 这也就意味着, **你不能填写两个参数, 实现一个方法同时监听两个事件的目的!** 63 | 64 | 这里我们用到了玩家交互事件. 这个事件抽象不易理解. 65 | 确切的来说, `PlayerInteractEvent`指的是玩家与方块交互, 交互指的是左右键方块的几乎一切操作. 具体的解释完全可以在JavaDoc中了解到. 66 | 如果你曾经用过领地插件`Residence`, 你肯定对某个领地的权限`use`印象很深, 这个`use`权限与`PlayerInteractEvent`事件差不多, 可以近似认为`Residence`插件的`use`权限就是通过监听`PlayerInteractEvent`写出来的. 67 | 68 | 要注意, **监听器必须要注册才能算生效**! 69 | 我们的监听器里的方法都能监听到对应的事件的原因是, 在`onEnable`方法中, 我们写了这样的代码: 70 | 71 | ```java 72 | Bukkit.getPluginManager().registerEvents(this,this); //这行代码注册了HelloWorld类为监听器, 如果没有这行代码, 下面所有带@EventHandler注解的方法都不会在事件触发时被调用! 73 | ``` 74 | 75 | *registerEvents方法的第一个参数是监听器,第二个参数是插件主类的实例. 在这里主类就是监听器. 具体你可以在后面了解到.* 76 | 77 | ## 理解客户端与服务端的关系 78 | 如果你实际去使用上面的那个代码, 你可能会发现一个问题: 玩家移动在游戏里还可以移动, 但是一会儿会被服务器"弹回来". 79 | 这样确实是达到了取消玩家移动的目的, 但是, 为什么最终的效果不是"玩家一点都动不了"呢? 80 | 81 | 事实上, 我们无法在服务端取消玩家一点也不能移动. 82 | 客户端移动玩家时, 会在客户端显示出移动后的样子, 然后才会传递给服务器玩家移动的信号, 服务端收到客户端的信号后, 服务器才会触发`PlayerMoveEvent`事件, 做出响应. 83 | 84 | 也就是说, 客户端与服务端之间, 客户端往往都是"先斩后奏"的. 客户端不管你服务端取不取消, 先那么显示出来再说. 85 | 86 | *如果要是真的想实现让玩家在服务器的某个坐标一点也动不了, 也许需要发挥你的聪明才智了. 让玩家卡在一个透明方块里? 也许有更好的方案? 现在有人已经实现了!* 87 | *目前我们通常利用设置玩家移动速度的方法来让玩家无法移动!* 88 | 89 | ## 查询我们想了解的事件 90 | 91 | ### 事件是怎么取名的 92 | 93 | 你可以发现, 玩家移动`PlayerMoveEvent`、玩家进入服务器`PlayerJoinEvent`事件都有明显的特征. 94 | 95 | 1. 功能决定名称, 看了名称你就能大致明白它的功能. 96 | 2. 都以`Event`作为结尾. 这也就说BukkitAPI中所有名字最后是`Event`的类都是事件类. 97 | 3. 开头的第一个词决定作用范围. 例如上面两个类开头都是`Player`, 这两个类都是与玩家有关的事件类. 98 | 99 | 所有的事件类都在`org.bukkit.event`包或其子包里. 100 | 101 | ### 可取消事件与不可取消事件怎么判断 102 | 例如`PlayerMoveEvent`在JavaDoc中, 我们可以注意到这些内容: 103 | 104 | ```java 105 | public class PlayerMoveEvent 106 | extends PlayerEvent 107 | implements Cancellable 108 | ``` 109 | 110 | `PlayerMoveEvent`事件实现了`Cancellable`接口. 111 | `Cancellable`中定义了`setCancelled`方法和`isCancelled`方法. 112 | 通过`setCancelled`方法, 你可以在事件触发时设置是否取消该事件. 例如, 如果监听玩家移动, 事件触发时使用`setCancelled`方法, 可以取消玩家移动. 113 | `isCancelled`方法可以判断该事件是否被取消. 114 | 115 | 对于不可取消事件, 它们没有实现`Cancellable`接口, 因此它们无法被取消. 116 | 就像玩家退出服务器, 你总不能像刀剑神域一样, 不让玩家退出服务器吧. 117 | 118 | *值得注意的是, 如果玩家并没有改变他的X/Y/Z, 而只是利用鼠标转了一下身, 这也属于玩家移动, 仍会触发`PlayerMoveEvent`事件.* 119 | 120 | ### 找到我们要找的事件 121 | 122 | 我们了解了如何监听事件, 那么我们想做到“不让玩家破坏方块”这个功能, 应该怎么做? 123 | 思考后可以发现, 我们需要监听“方块被破坏”这个事件!那破坏方块后触发什么事件? 你需要在JavaDoc中找才能找到! 124 | 125 | 分析: 破坏方块这个事件是一个与方块有关的事件. 打开JavaDoc你可以发现`BlockXXXXEvent`这类的类有许多. 126 | 你也许会说, 玩家破坏方块为什么不是一个与玩家有关的事件呢?很有道理!你也可以在玩家事件中找找看有没有这样的事件. 127 | 128 | JavaDoc左侧上方是所有的包, 点击`org.bukkit.event.block`就能在左侧下方看所有与方块有关的事件了. 129 | 你可以轻松地发现, 在前几个的位置迅速就能看到`BlockBreakEvent`, 根据名字就能判断出, 这就是你想找的方块破坏事件, 打开后看到描述为`Called when a block is broken by a player.`, 很明显, 监听它就对了. 130 | 131 | ```java 132 | @EventHandler 133 | public void onBlockBreak(BlockBreakEvent e) { 134 | e.setCancelled(true); 135 | } 136 | ``` 137 | 138 | 这样我们就写出了想要的功能. 139 | 140 | ### 并不是所有的事件都能监听. 141 | 142 | 在查阅JavaDoc时你可能发现`PlayerEvent`、`BlockEvent`这种事件.这些都是不可以被监听的事件. 143 | 你不可以通过监听`PlayerEvent`事件来达到一次性监听所有与玩家有关的事件的目的. 144 | *它们不能被监听的原因是没有做HandlerList. 在这里不多说明, 后面讲述如何自己做一个自定义事件时你会明白.* 145 | 146 | 一般来说,如果事件名由两个词构成(例如`PlayerEvent`)都不能监听, 大多数事件都可以监听. 147 | 148 | 你可能好奇, 常见的登录插件都是把所有需要的玩家事件都写了`@EventHandler`注解方法一个个监听的? 149 | 答案是, 的确如此. 你要想写登录插件, 你就应该去监听许许多多事件, 累也没办法, 就得这样写. 150 | 151 | ## EventHandler注解的参数 152 | ##监听优先级 153 | 想象一下, 如果有两个插件, 他们同时监听玩家移动. 其中一个插件判断后发现玩家没有充够450块钱, 于是它取消了这名玩家的移动. 但是另外一个插件判断后发现玩家非常帅, 于是它允许了这名玩家的移动. 154 | 那么就会存在问题: 有一个插件`setCancelled(true)`, 而又有插件`setCancelled(false)`. 应该以谁为准? 155 | 那就要看监听优先级了! 156 | 157 | 下面是两个插件处理`PlayerMoveEvent`的部分: 158 | A插件: 159 | ```java 160 | // A插件 161 | @EventHandler(priority=EventPriority.LOWEST) 162 | public void onPlayerMove(PlayerMoveEvent e) { 163 | System.out.println("testA"); 164 | e.setCancelled(true); 165 | } 166 | ``` 167 | B插件: 168 | ```java 169 | // B插件 170 | @EventHandler(priority=EventPriority.HIGHEST) 171 | public void onPlayerMove(PlayerMoveEvent e){ 172 | System.out.println("testB"); 173 | e.setCancelled(false); 174 | } 175 | ``` 176 | 在实际的运行中, 当玩家移动时你会发现, 控制台中先输出了`testA`后输出了`testB`, 玩家都在服务器内可以自如移动. 177 | 这意味着A插件第一个响应了玩家移动, 然后B插件才相应的玩家移动. 178 | `@EventHandler`注解有一个成员叫做`priority`, 给他设置对应的`EventPriority`, 即可设置监听优先级. 在上面的例子中, Bukkit会在所有的LOWEST级监听被调用完毕后, 再去调用HIGHEST级监听. 179 | 180 | `EventPriority`提供了五种优先级, 按照被调用顺序,为: 181 | LOWEST < LOW < NORMAL(如果你不设置, 默认就是它) < HIGH < HIGHEST < MONITOR . 182 | 其中, LOWEST最先被调用, 但对事件的影响最小. MONITOR最后被调用, 对事件的影响最大. 183 | 184 | ### ignoreCancelled 185 | `@EventHandler`注解除了`priority`之外, 还有`ignoreCancelled`. 如果不设置, 它默认为false. 186 | 187 | 让我们回到上面的A插件与B插件的例子中. 我们把B插件的`onPlayerMove`改成这样: 188 | ```java 189 | // B插件 190 | @EventHandler(priority=EventPriority.HIGHEST, ignoreCancelled = true) 191 | public void onPlayerMove(PlayerMoveEvent e) { 192 | System.out.println("testB"); 193 | e.setCancelled(false); 194 | } 195 | ``` 196 | 可以发现, 后台只输出了`testA`, 玩家无法在服务器中移动. 这说明B插件的`onPlayerMove`没有被触发. 197 | 如果有其他监听已经取消了该事件, 设置`ignoreCancelled`为`true`将可以忽略掉这个事件, 所以B插件的`onPlayerMove`方法没有被触发. 198 | 199 | ## 监听器的注册 200 | 可能你已经发现了, 在之前的代码中, 我们都会在`onEnable`方法中插入这样的语句: 201 | ```java 202 | Bukkit.getPluginManager().registerEvents(this,this); 203 | ``` 204 | 当时解释的是, `registerEvents`方法注册了该监听器. 205 | 如果没有这样的注册语句, 那么Bukkit就不会在事件触发时调用监听器类的对应方法. 206 | 207 | 该方法的第一个参数是监听器, 第二个参数是插件主类的实例. 当时由于我们为了偷懒, 直接把主类实现了`Listener`作为监听器, 因此我们可以这样写. 208 | 可我们不能写插件的时候把代码都堆在主类中. 这也就意味着, 我们可以把其他类实现`Listener`, 用同样的方式注册它, 这样我们就可以把监听事件部分的代码放在别的地方, 使插件代码更有条理性. 209 | 210 | 我们新创建一个类, 让它实现`Listener`, 再写对应的方法监听玩家移动, 就像这样: 211 | ```java 212 | public class DemoListener implements Listener { 213 | @EventHandler 214 | public void onPlayerMove(PlayerMoveEvent e) { 215 | System.out.println("PLAYER MOVE!"); 216 | } 217 | } 218 | ``` 219 | 现在我们在主类的`onEnable`方法里, 就可以注册它了! 220 | ```java 221 | Bukkit.getPluginManager().registerEvents(new DemoListener(), this); 222 | ``` 223 | 224 | ## 常用事件简介 225 | 226 | 这里可能罗列不会全面, 在我想到哪些“坑事件”后会列在这里. 227 | 228 | ### 登录、进入服务器 229 | BukkitAPI中与登录有关的常见的有: `PlayerLoginEvent` `PlayerJoinEvent`. 230 | 值得注意的是, 所有玩家进入服务器的事件都是不可取消事件. 231 | 232 | 在玩家尝试连接服务器时, 会触发`PlayerLoginEvent`, 玩家完全地进入服务器后, 会触发`PlayerJoinEvent`. 233 | 在`PlayerLoginEvent`触发的时候, 你不可以操控玩家`Player`对象获取其背包等信息, 而仅可以获取UUID、玩家名和网络信息(IP等)等. 234 | *顺便一提, 玩家如果不在线, 你不可以通过BukkitAPI操控其背包. * 235 | `PlayerJoinEvent`触发时, 服务器内将会出现玩家实体. 此时你可以当做玩家完全进入服务器, 对其自由操作. 236 | 237 | 打个比方, 你家有一扇防盗门, 有人想进入你家. 238 | 首先他需要敲门, 在门外喊出自己的基本信息(名字等), 这是`PlayerLoginEvent`触发的时候. 如果你想从他背包里拿出东西, 不可以, 因为他在门外面. 239 | 当你给他打开门, 他进了你家中站稳了以后, 这是`PlayerJoinEvent`触发的时候, 这时候不管你是想打他还是想拿走他的东西, 都可以. 240 | 241 | ### 玩家移动 242 | 在上面我们已经提及过, 玩家移动是“先斩后奏”被触发的. 具体请见上文. 243 | 244 | ### 玩家打开背包 245 | 也许你会看到`InventoryOpenEvent`. 根据描述你大概明白, 类似右击箱子后出现的那种带格子的界面被打开可以被监听. 246 | 但是有一件事很重要: 玩家按E打开背包是没办法被监听的. 247 | 248 | 一般如果要实现禁止玩家打开背包, 其实最常规的做法就是开一个`BukkitRunnable`, 定时调用`p.closeInventory()`关闭玩家正在打开的背包实现的. 249 | *这里不详细讲述具体如何操作, 感兴趣可以在QQ群中问一些有经验的开发者.* 250 | *后面会讲述Runnable, 也许看后你会明白如何操作.* 251 | 252 | 感兴趣可以看看这个帖子: https://www.mcbbs.net/thread-965760-1-1.html 253 | -------------------------------------------------------------------------------- /unit/2-3.md: -------------------------------------------------------------------------------- 1 | # 配置API 2 | 3 | 配置文件用来储存配置信息, 以便使用文件开关功能、储存数据、修改信息. 4 | 我们往往需要读写配置文件. Bukkit为我们提供了配置API. 5 | 6 | 配置API是BukkitAPI提供的读写配置文件的工具. 其相对而言较为简单, 是插件开发中常用的API. 7 | 8 | *目前为止, 配置API只有YAML配置功能可用. 这也是大多数插件为什么配置文件是YAML文件的原因. 9 | 在本文中, 我们也将使用YAML配置API.* 10 | *现在的配置API的类均在 `org.bukkit.configuration` 和 `org.bukkit.configuration.file` 包中.* 11 | 12 | ## 了解YAML文件 13 | 14 | ### 键值对 15 | 16 | 相信开服的经验已经使你对YAML文件有了初步认识. 17 | YAML文件的文件后缀是`.yml`. 其配置文件结构需要严格遵守YAML标准. 18 | 19 | 下面是一个符合标准的YAML配置文件的内容: 20 | 21 | ```yaml 22 | Settings: 23 | DebugMode: true 24 | Time: 25 | CoolDown: 10 26 | Data: 27 | player1: 28 | NickName: HandsomeBoy 29 | Score: 50 30 | TotalTime: 40 31 | Title: 32 | - Toilet Protecter 33 | - Widow Maker 34 | - Chicken Fucker 35 | ``` 36 | 37 | 相信你可以**根据空格看出每个项目之间的所属关系**, 如下: 38 | 39 | ![](https://i.loli.net/2020/07/11/TiWXG8mCUBMtcY3.jpg) 40 | 41 | **我们把上面所属关系图中, 矩形框内的东西叫做键(Key)**. 例如, `Settings`是一个键, `Data`是个键. **在`Settings`键下存在`DebugMode`、`Time`两个子键, 它们分别叫做`Settings.DebugMode`键和`Settings.Time`键**. 同理, 在`Settings.Time`键下还有`CoolDown`这个子键, 这个子键叫`Settings.Time.CoolDown`键. 42 | 43 | 我们可以用这样的命名方法来称呼一个YAML文件中的任一一个键了. 并且还可以根据名称看出所属关系. 44 | 例如, `Data.player1.Score`键对应的值是 `50`. 45 | 46 | 在YAML中, 键和值一一对应, 一个键一定会有一个值. 47 | 48 | ### 数据类型 49 | 50 | 通常可以用配置文件存储一些基本类型(int、double、boolean)、String、数组和可被序列化的对象. 51 | 52 | Bukkit中给出的一些对象有些是可以直接存进配置文件的, 这需要看这个类是不是实现了`ConfigurationSerializable`接口. 例如, `Player`类型的对象就可以被直接存入配置文件, 因为查阅JavaDoc后可以发现它实现了`ConfigurationSerializable`. 53 | 54 | ![](pics/1-4-pic2.jpg) 55 | 56 | *后续会详细介绍, 这里需要知道判断方法.* 57 | 58 | 在上面的配置文件中, 配置文件里储存了: 59 | 1. 存储了一个`boolean`类型的值(`Settings.DebugMode`键). 60 | 2. 存储了一些数字类型的值. 61 | 3. 存储了一个`String`字符串(`Data.player1.NickName`键). 62 | 4. 存储了一个`StringList`(YAML里的`StringList`就是Java中的`List`, 例如`Data.player1.Title`键). 63 | 64 | YAML中注释以`#`表示. 65 | ```yaml 66 | #就像这样写注释, 配置文件读取时会忽略掉注释 67 | Settings: 68 | DebugMode: true 69 | ``` 70 | 71 | 相信你可以通过这个例子明白配置文件中可以储存哪些数据了. 72 | 73 | ### 对于不存在的数据 74 | 75 | 很明显, 上面的配置文件中, 并没有`Data.player2.NickName`键, 那么如果我非要获取`Data.player2.NickName`键的值, 获取到的数据是什么呢? 76 | 答案是null. 换句话说, **YAML里所有不存在的键, 值是null.** 77 | 78 | 请记住这句话. 我们可以根据这个原理推导出, 如果你想删除一个已经存在的键, 那就是把这个键的值设置为null. 79 | 80 | ## 操作默认配置文件 81 | 82 | 这里的默认配置文件指的是`config.yml`文件. 83 | 首先我们需要准备一个默认的`config.yml`文件. 这个文件会在插件检测到`plugins\插件名`文件夹下没有`config.yml`文件时被放入该文件夹中. 84 | 在插件jar文件里, 默认的`config.yml`文件要与`plugin.yml`文件处于同一目录下, 所以创建默认`config.yml`的方法与创建`plugin.yml`文件的操作方法一致. 在这里我们在默认`config.yml`文件中存入我们一开始举的例子. 85 | 86 | ### 读取config.yml数据 87 | 下面做一个插件, 在玩家登陆服务器时, 给玩家显示配置文件`Data.玩家名.Score`键对应的值. 88 | 89 | ```java 90 | public class HelloWorld extends JavaPlugin implements Listener{ 91 | public void onEnable(){ 92 | saveDefaultConfig(); //这个代码会自动判断插件配置文件里是不是有config.yml, 没有就会放入默认的config.yml 93 | Bukkit.getPluginManager().registerEvents(this,this); 94 | } 95 | 96 | public void onDisable(){} 97 | 98 | @EventHandler 99 | public void onPlayerJoin(PlayerJoinEvent e){ 100 | //在这里我们监听了PlayerJoinEvent, 并操作`config.yml` 101 | String key = "Data." + e.getPlayer().getName() + ".Score"; //这是我们要获取的键名 102 | int score; 103 | if(getConfig().contains(key)){ //先判断一下有没有这个键 104 | score = getConfig().getInt(key); //有的话读取 105 | } else { 106 | score = 0; //没有的话就按0处理 107 | } 108 | e.getPlayer().sendMessage("你的积分是: " + score); //然后给玩家发送 109 | } 110 | } 111 | ``` 112 | 113 | 如果你用`getConfig().getString(key)`获取玩家数据`Score`键的值, 那么获取到的就是一个String字符串. 114 | 也就是, YAML中值对应的数据类型具体是什么, 关键要看你用的getter是什么. 115 | 116 | ### 写入数据到config.yml 117 | 118 | 我们再来做个"加分项", 玩家挖掉一个石头后, 给他加分. 119 | 120 | ```java 121 | public class HelloWorld extends JavaPlugin implements Listener{ 122 | public void onEnable(){ 123 | saveDefaultConfig(); 124 | Bukkit.getPluginManager().registerEvents(this,this); 125 | } 126 | 127 | public void onDisable(){} 128 | 129 | @EventHandler 130 | public void onPlayerJoin(PlayerJoinEvent e){ 131 | //这里代码跟上面是一模一样的, 这里只是做了简化, 因为原先的if占篇幅太大 132 | String key = "Data." + e.getPlayer().getName() + ".Score"; 133 | int score = getConfig().contains(key)?getConfig().getInt(key):0; 134 | e.getPlayer().sendMessage("你的积分是: " + score); 135 | } 136 | 137 | @EventHandler 138 | public void onBlockBreak(BlockBreakEvent e){ 139 | if (e.isCancelled()) return; //判断此事件是不是被其它插件取消掉了 140 | if(e.getBlock().getType() == Material.STONE){ //判断类型, 是石头 141 | String key = "Data." + e.getPlayer().getName() + ".Score"; 142 | int score = getConfig().contains(key)?getConfig().getInt(key):0; //获取玩家当前积分, 如果从未记录此玩家的积分数据则默认为0 143 | getConfig().set(key,score + 10); //挖一个石头加10分 144 | 145 | //但是写到这里要小心!你只是修改了内存上的数据, 你没有修改硬盘上的config.yml文件里的数据! 146 | saveConfig(); //所以要注意, 修改数据要记得保存 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | 由此, 你需要小心, **getConfig()的内容是内存上的内容, 修改它并没有修改硬盘上的内容, 关服/重载后就会消失, 因此要注意保存!** 153 | 154 | `set`不区分数据类型是什么, 存储数据全部都用`set`方法. `set`不管这个键在配置文件里存不存在, 都会写入这个数据. 155 | 156 | 还记得我们一开始说的`YAML里所有不存在的键, 值是null`吗? 如果你想删除掉`player3`的数据, 那你应该写成: 157 | 158 | ```java 159 | getConfig().set("Data.player3",null); 160 | ``` 161 | 162 | 这样配置文件里`Data`键下就没有`player3`的数据了,也就达到了删除一个键的目的. 163 | 164 | ## 操作自定义的配置文件 165 | 关于非`config.yml`的YAML文件的操作, 有很多种方式可以做到. 166 | 下文叙述的是其中的一种. 167 | 168 | ### 准备默认配置文件 169 | 我们还是需要像`config.yml`那样准备一份默认配置文件, 放在与plugin.yml相同目录下. 不同的是, 除了`saveDefaultConfig`以外, 我们还需要其他的代码来保存默认配置文件. 170 | 171 | 例如我们有`config.yml`和`biu.yml`两个配置文件, 插件加载时应该这样生成默认配置文件: 172 | ```java 173 | this.saveDefaultConfig(); //生成默认config.yml 174 | this.saveResource("biu.yml", false); //生成默认biu.yml 175 | ``` 176 | *`saveResource`方法的第一个参数是文件名, 第二个参数是是否覆盖, 设置成false可以达到saveDefaultConfig的效果.* 177 | 178 | 同理,利用`saveResource`可以生成你想生成的默认的非`config.yml`的配置文件. 179 | 180 | 如果我想实现在插件配置文件夹创建一个新的文件夹存放配置文件怎么做呢? 很简单: 181 | ``` 182 | this.saveResource("test\biu.yml", false); //生成默认biu.yml, 放在test文件夹里, Jar文件中也需要有test文件夹 183 | ``` 184 | 185 | ### 基本读写与保存 186 | 下面是一个读写与保存的示例: 187 | ```java 188 | // 读取配置文件 189 | // this.getDataFolder()方法返回插件配置文件夹的File对象 190 | File biuConfigFile = new File(this.getDataFolder(), "biu.yml"); 191 | // 也可以在插件配置文件夹创建一个新的文件夹以存放配置文件 192 | // File biuConfigFile = new File(this.getDataFolder(), "test/biu.yml"); 193 | FileConfiguration biuConfig = YamlConfiguration.loadConfiguration(biuConfigFile); 194 | biuConfig.get....... 195 | biuConfig.set....... 196 | // set完了记得保存! 197 | biuConfig.save(biuConfigFile); 198 | -------------------------------------------------------------------------------- /unit/2-4.md: -------------------------------------------------------------------------------- 1 | # 命令执行器 2 | 3 | ## 认识命令机制 4 | MC中的命令是一个字符串, 用来实现游戏内高级功能. 5 | 6 | 在MC客户端中, 玩家将在聊天框内输入命令. 7 | **当且仅当在“聊天”内, 命令与普通的聊天内容的区别在于其内容的第一个字符是一个斜杠`/`**. 8 | 9 | 该字符串中的空格表示一个分隔, 开头的一节为命令的名称. 10 | 除去命令的名称, 剩下的部分从空格处断开可以分成一个数组. 11 | 12 | 例如, `a b c`是一个命令, 其命令名称为`a`, 其参数可用一个数组`args`表示为: 13 | ``` 14 | args[0]: "b" 15 | args[1]: "c" 16 | ``` 17 | 18 | 19 | 20 | ## 定义新命令 21 | 如果我们需要定义一个新的命令, 首先我们需要在`plugin.yml`文件中增加相关信息: 22 | ```yml 23 | name: HelloWorld 24 | main: tdiant.helloworld.HelloWorld 25 | version: 1 26 | author: tdiant 27 | commands: 28 | rua: 29 | description: RUA!RUA!RUA! 30 | ``` 31 | 32 | 在`plugin.yml`文件里, 我们增加了`commands.rua`键, 这就可以代表注册了一个`rua`命令. 我们给他增加了一个`description`子键表示对该命令的描述, 描述信息会出现在`/help`菜单里. 33 | 34 | 请注意, 请尽可能不要在plugin.yml文件里出现中文! 这可能会出现问题! 35 | 36 | `commands.命令名`键可以有很多个子键, 这些都不是必须添加的, 甚至它可以没有子键. 具体子键如下: 37 | 38 | | 键 | 用途 | 例子 | 39 | | ----- | ----- | ---- | 40 | | description | 描述作用. 将会在/help中显示 | description: "I am a cute command." | 41 | | aliases | 设置别名. 比如登录插件login命令也可以用/l命令代替. | aliases: [l, log] | 42 | | permission | 设置命令需要的权限 | permission: rua.use | 43 | | permission-message | 没权限时的提示语 | permission-message: "YOU HAVE NO PERMISSION!" | 44 | | usage | 命令的用法. | usage: `/ YOUR_NAME` | 45 | 46 | 47 | 注意: 48 | 1. ``在usage里可以代表你的命令名. 49 | 2. 你的命令设置了aliases后命令名不能按照aliases称呼. 比如你给login命令设置了`aliases: [l]`你不能也叫他`l`命令, 它还是`login`命令. 50 | 3. 不推荐使用`permission`和`permission-message`, 因为plugin.yml里出现中文爱出问题. 事实上, 我们可以用`Player.hasPermission`方法在监听命令的时候自己亲自判断有没有权限. 51 | 4. 如果一个名称被别的插件注册了或设置为了某个命令的别称, 会出现冲突问题, 尽量避免. 52 | 5. 别弄中文的命令, 如果想搞, 去试试监听`PlayerCommandPreprocessEvent`. 53 | 54 | ## onCommand 55 | 我们可以类似`Listener`, 做一个`CommandExecutor`监听命令. 56 | 57 | ```java 58 | public class DemoCommand implements CommandExecutor { 59 | @Override 60 | public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { 61 | sender.sendMessage("HI!"); 62 | return true; //true代表命令执行没问题, 返回false的话Bukkit会给命令输入方一个错误提示语 63 | } 64 | } 65 | ``` 66 | 67 | 然后也同理, 在onEnable里加入注册: 68 | ```java 69 | Bukkit.getPluginCommand("rua").setExecutor(new DemoCommand()); 70 | ``` 71 | 但是如果onCommand方法放在了主类里, 那就不需要注册了. 72 | 73 | `onCommand`方法有四个参数, 分别为: 74 | 1. `CommandSender sender` —— 命令输入方, 实际传入的有可能是Console, 有可能是Player或者其他情况. 75 | 2. `Command cmd` —— 所执行的命令对象. 76 | 3. `String label` —— 如果该指令被设置了别名(`aliases`), 此值将为玩家使用哪一个别名执行了该指令. 77 | 4. `String[] args` —— 参数. 例如/rua a b的话, args[0]为"a", args[1]为"b". 78 | 79 | > 警告: 字符串的比较, 请不要使用`==`, 因为其比对的是内存地址, 可能造成一些没有预料到的结果! 建议使用`equals`方法, 例如`args[0].equals(string)` 80 | 81 | 如果你的命令希望只被玩家使用, 通常这样判断: 82 | ```java 83 | if(!(sender instanceof Player)){ 84 | sender.sendMessage("你不是玩家!不能用!"); 85 | return true; //不返回true, Bukkit还会显示出来一串错误提示, 你可以试试看. 86 | } 87 | ``` 88 | 89 | 判断完为玩家后, 若希望判断其有没有权限执行命令, 可以: 90 | ```java 91 | Player p=(Player)sender; //sender可以直接强转为Player 92 | if(p.hasPermission("rua.use")){ 93 | p.sendMessage("你有权限!"); 94 | } 95 | ``` 96 | 玩家将会在聊天区域内看到输出: 97 | ``` 98 | 你有权限! 99 | ``` 100 | 101 | Bukkit内可以用ChatColor表示颜色前缀, 例如: 102 | ```java 103 | p.sendMessage(ChatColor.RED+"你输错了!"); //输出红色的 "你输错了" 104 | p.sendMessage(ChatColor.RED+"还可以"+ChatColor.YELLOW+"两种颜色混着用!"); 105 | p.sendMessage(ChatColor.BOLD+"猜猜我会显示成什么效果"); 106 | p.sendMessage(ChatColor.RED+""+ChatColor.BOLD+"猜猜我会显示成什么效果"); 107 | p.sendMessage(ChatColor.BOLD+""+ChatColor.RED+"猜猜我会显示成什么效果"); 108 | 109 | String str = "&4哈哈"; //假如你从配置文件里读出来了一串 "&4哈哈". 110 | p.sendMessage(str); //这样会显示出 "&4哈哈", 不带颜色 111 | p.sendMessage(ChatColor.translateAlternateColorCodes('&',str)); //这样就带颜色了 112 | ``` 113 | 114 | 还有其他的好玩的东西, 把下面的代码放在onEnable方法里试试看: 115 | ```java 116 | System.out.println(ChatColor.RED+"猜猜我是什么效果"); 117 | this.getLogger().info(ChatColor.RED+"你再猜猜我是什么效果"); 118 | ``` 119 | 以后推荐您用`getLogger().info`方法代替`System.out.println(也就是sout、sysout方法)`! 120 | 121 | 在实际应用的时候, 还要小心`args.length`! 玩家只输入`/rua`没有参数的时候, 小心因为自己的疏忽造成`ArrayIndexOutOfBoundsException`! 122 | 123 | -------------------------------------------------------------------------------- /unit/3-1.md: -------------------------------------------------------------------------------- 1 | # Bukkit类与箱子GUI的实现 2 | 3 | 服务器里经常会利用箱子的GUI做“按钮菜单”功能. 有些服务器可能利用`ChestCommand`插件做出了各种花样的菜单. 4 | 如何写一个插件来实现这样的箱子GUI呢? 5 | 6 | # Bukkit类 7 | 8 | 我们早在事件监听注册监听器时就已经见过`Bukkit`类了. 9 | 10 | ```java 11 | Bukkit.getPluginManager().registerEvents(this,this); 12 | ``` 13 | 14 | `Bukkit`类是服务器的单例. 我们可以通过它操作服务器. 15 | 例如, 你可以用`Bukkit.banIP("某个IP")`来封禁某个IP号. 更多的用法可以在JavaDoc上查到. 16 | 17 | 你也可以利用`Server`对象操作服务器, 二者几乎没有差别(`Bukkit`类内部就是操作`Server`对象). 18 | 插件主类提供`getServer()`方法, 返回值就是一个`Server`对象. 19 | 20 | # Inventory的使用 21 | 22 | 箱子GUI本质是一个Inventory界面. 首先我们需要创建一个Inventory对象出来. 23 | 但我们不必直接`new Inventory(...)`, `Bukkit`类给我们提供了创建`Inventory`对象的方法: 24 | 25 | ```java 26 | Inventory inv = Bukkit.createInventory(player, 6*9, "URARA!"); 27 | //第一项是主人, 在这里可以设打开界面的玩家Player对象(还记得Inventory和箱子或玩家背包等一一对应吗) 28 | //第二项必须是 9n (n是1≤n≤6的正整数) 29 | //第三项是标题 30 | ItemStack item_bk = new ItemStack(Material.DIAMOND); 31 | 32 | //在四周设置钻石边框 33 | //这里用这样脑残的写法是为了告诉你一个大概的意思 34 | //我相信你实际写的时候不会这么简单粗暴解决问题的, 应该会用上循环解决, 对吧 35 | inv.setItem(0,item_bk); 36 | inv.setItem(1,item_bk); 37 | inv.setItem(2,item_bk); 38 | inv.setItem(3,item_bk); 39 | inv.setItem(4,item_bk); 40 | inv.setItem(5,item_bk); 41 | inv.setItem(6,item_bk); 42 | inv.setItem(7,item_bk); 43 | inv.setItem(8,item_bk); 44 | inv.setItem(9,item_bk); 45 | inv.setItem(17,item_bk); 46 | inv.setItem(18,item_bk); 47 | inv.setItem(26,item_bk); 48 | inv.setItem(27,item_bk); 49 | inv.setItem(35,item_bk); 50 | inv.setItem(36,item_bk); 51 | inv.setItem(44,item_bk); 52 | inv.setItem(45,item_bk); 53 | inv.setItem(46,item_bk); 54 | inv.setItem(47,item_bk); 55 | inv.setItem(48,item_bk); 56 | inv.setItem(49,item_bk); 57 | inv.setItem(50,item_bk); 58 | inv.setItem(51,item_bk); 59 | inv.setItem(52,item_bk); 60 | inv.setItem(53,item_bk); 61 | 62 | ItemStack item_button1 = new ItemStack(Material.GOLD); 63 | ItemStack item_button2 = new ItemStack(Material.ANVIL); 64 | inv.setItem(22,item_button1); 65 | inv.setItem(31,item_button2); 66 | 67 | //然后可以给玩家打开这个Inventory(注意, 我们还没做限制, 这个时候玩家可以自由的在这个GUI里拿东西出来) 68 | p.openInventory(inv); 69 | ``` 70 | 71 | 效果大概是这样的: 72 | ![](http://www.miao.su/images/2018/08/15/QQ201808151748188c576.png) 73 | 74 | 然后我们监听`InventoryClickEvent`实现功能和限制: 75 | ```java 76 | @EventHandler 77 | public void onInventoryClick(InventoryClickEvent e){ 78 | //从这里可以看出来, 标题不是随意设置的, 我们经常用标题作为区分GUI的标志 79 | if(e.getWhoClicked().getOpenInventory().getTitle().equals("URARA!")){ 80 | e.setCancelled(true); //这样玩家就没办法拿出来物品了 81 | 82 | //getRawSlot获得玩家点击的格子编号 83 | //但是玩家点击GUI之外不是格子的地方也会触发InventoryClickEvent, 需要做处理! 84 | if(e.getRawSlot()<0 || e.getRawSlot()>e.getInventory().getSize() || e.getInventory()==null) 85 | return; 86 | 87 | //自从Mojang把HIM删掉以后, 能触发InventoryClickEvent的只有Player了 88 | //目前来说可以直接把它强转成Player 89 | Player p = (Player)e.getWhoClicked(); 90 | 91 | if(e.getRawSlot()==22){ 92 | p.sendMessage("你点击了金锭!"); 93 | p.closeInventory(); 94 | } else { 95 | p.sendMessage("你没有点击金锭!"); 96 | p.closeInventory(); 97 | } 98 | } 99 | } 100 | ``` 101 | 基于这个思路, 你可以做出一个有功能的箱子GUI了! 102 | 103 | > 思考: 如果遇到了某些能够修改箱子GUI的标题的插件(比如帮助加前缀) 104 | > 能不能利用 holder 来区分GUI呢? 105 | 106 | -------------------------------------------------------------------------------- /unit/3-10.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/3-11.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/3-2.md: -------------------------------------------------------------------------------- 1 | # 自定义事件 2 | 3 | 我们现在所了解的事件都是Bukkit提供的. 例如, 玩家移动等. 4 | 那如果我们想自己去做一个事件呢? 5 | 6 | ## 按需创建类 7 | 8 | 比如, 我想自己做出来一个`RuaEvent`, 实现在玩家聊天说`rua`的时候触发. 9 | 很明显, Bukkit只会提供玩家发送聊天信息的事件, 肯定不会单独为了实现在玩家聊天发送`rua`的时候单独做个事件. 那应该怎么做? 10 | 11 | 首先想到的应该是监听玩家聊天事件, 然后判断玩家聊天发送的内容是什么, 如果是`rua`做我想做的事情. 这是常规的解决方法. 12 | 但是如果我想做一个强化插件, 我想在玩家强化物品的时候触发一个事件给自己和其他插件, 那我应该怎么做? 不如自定义一个属于自己的事件! 13 | 14 | 这里我们以创建上文的`RuaEvent`事件举例, 我们的大致思路是这样的: 15 | 1. 创建一个`RuaEvent`类. 16 | 2. 监听玩家聊天, 判断玩家聊天内容, 如果是`rua`, 让Bukkit触发我们新建的`RuaEvent`对象. 17 | 3. 向玩家发送消息`rua`. 18 | 19 | 我们就先新建一个类`RuaEvent`, 让其继承`org.bukkit.event.Event`类. 在该类中写下这些固定代码: 20 | ```java 21 | public class RuaEvent extends Event{ 22 | private static final HandlerList handlers = new HandlerList(); 23 | @Override 24 | public HandlerList getHandlers() { 25 | return handlers; 26 | } 27 | 28 | public static HandlerList getHandlerList() { 29 | return handlers; 30 | } 31 | } 32 | ``` 33 | HandlerList储存与监听本事件的监听器相关的对象. 34 | 这意味着Bukkit中注册监听器的本质就是在每个对应的事件HandlerList中加入该监听器的有关对象. 35 | 这也意味着Bukkit中事件的触发本质是遍历被触发事件的HandlerList, 调用监听器对应方法. 36 | 37 | > 假如我想让服务器里的玩家触发的所有事件, 已知所有的诸如PlayerJoinEvent等玩家事件都继承了PlayerEvent, 那我可以监听PlayerEvent事件吗? 38 | > 答案是不可以, 因为`PlayerEvent`没有`getHandlerList()`方法, 结合上面的内容, 你应该可以意识到PlayerEvent是无法正常工作的吧. 39 | > 所以你只能把所有Player开头的Event监听一个遍才可以达到目的! 40 | 41 | 现在我们的自定义事件雏形已经完成. 你可以根据自己的需要添加相关代码! 42 | 这里我们示例的`RuaEvent`代码最终如下: 43 | 44 | ```java 45 | public class RuaEvent extends Event { 46 | private static final HandlerList handlers = new HandlerList(); 47 | private Player p; 48 | 49 | public RuaEvent(Player p){ 50 | this.p = p; 51 | } 52 | 53 | public Player getPlayer(){ 54 | return p; 55 | } 56 | 57 | @Override 58 | public HandlerList getHandlers() { 59 | return handlers; 60 | } 61 | 62 | public static HandlerList getHandlerList() { 63 | return handlers; 64 | } 65 | } 66 | ``` 67 | 68 | ## 可取消事件的实现 69 | 70 | 等一等, 这样做出来的事件没有`setCancelled`方法和`isCancelled`方法, 这是不可取消的事件. 71 | 如果想做成可取消事件, 需要实现`Cancellable`接口: 72 | ```java 73 | public class RuaEvent extends Event implements Cancellable{ 74 | private static final HandlerList handlers = new HandlerList(); 75 | private Player p; 76 | 77 | private boolean cancelledFlag = false; 78 | 79 | public RuaEvent(Player p){ 80 | this.p = p; 81 | } 82 | 83 | public Player getPlayer(){ 84 | return p; 85 | } 86 | 87 | @Override 88 | public HandlerList getHandlers() { 89 | return handlers; 90 | } 91 | 92 | public static HandlerList getHandlerList() { 93 | return handlers; 94 | } 95 | 96 | @Override 97 | public boolean isCancelled() { 98 | return cancelledFlag; 99 | } 100 | 101 | @Override 102 | public void setCancelled(boolean cancelledFlag) { 103 | this.cancelledFlag = cancelledFlag; 104 | } 105 | } 106 | ``` 107 | 108 | 如果是不可取消的事件, 无需实现`Cancelled`. 109 | 截止到现在, `RuaEvent`已经自定义成功, 现在我们只需要做第二步即可: 110 | 111 | 1. 如果RuaEvent是个不可取消事件 112 | 113 | ```java 114 | @EventHandler 115 | public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个不可取消事件 116 | if(e.getMessage().equals("rua")) Bukkit.getServer().getPluginManager().callEvent(new RuaEvent(e.getPlayer())); //触发事件 117 | e.sendMessage("Rua!"); 118 | } 119 | ``` 120 | 121 | 2. 如果RuaEvent是个可取消事件 122 | 123 | ```java 124 | @EventHandler 125 | public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个可取消事件 126 | if(e.getMessage().equals("rua")){ 127 | RuaEvent event = new RuaEvent(e.getPlayer()); 128 | Bukkit.getServer().getPluginManager().callEvent(event); 129 | if(event.isCancelled()) { 130 | return; //事件被取消, 终止事件的处理 131 | } 132 | // 事件未取消对应的逻辑 133 | e.sendMessage("Rua!"); 134 | } 135 | } 136 | ``` 137 | 138 | *在这里监听了**PlayerChatEvent**,但是此事件已被标记@Deprecated,实际的开发过程中不推荐监听此事件.* 139 | *实际开发中建议监听的是**AsyncPlayerChatEvent**事件. 注意这是异步监听,用法基本类同于上述事件的监听,具体请参见JavaDoc.* 140 | -------------------------------------------------------------------------------- /unit/3-3.md: -------------------------------------------------------------------------------- 1 | # 深入plugin.yml 2 | 3 | `plugin.yml`文件是Bukkit及其衍生服务端识别插件的重要文件. 4 | 5 | 在服务端加载插件时, 服务端加载完毕Jar文件后做的第一件事就是读取该Jar文件的`plugin.yml`文件. 6 | 如果把任一可正常工作的插件的Jar文件用相应的ZIP压缩软件打开, 删除`plugin.yml`文件后再启动服务端, 会抛出错误. 7 | ``` 8 | Could not load 'plugins\[YOUR_PLUGIN].jar' in folder 'plugins' 9 | org.bukkit.plugin.InvalidDescriptionException: Invalid plugin.yml 10 | ``` 11 | 可发现, 服务端将会因为没有`plugin.yml`文件而抛出`InvalidDescriptionException`错误. 12 | 13 |
14 | 15 | 在`plugin.yml`文件中, 目前我们已知的有`name`、`version`、`main`、`author`四个项目可以设置. 16 | 事实上, `plugin.yml`文件中还有许多可以设置的项目, 部分项目是本节的内容, 其余可以在SpigotMC的官方文档中查阅到. 17 | 18 | > 目前BukkitAPI主要由SpigotMC维护, 因此大量的BukkitAPI文档都在 SpigotMC 网站上. 19 | > 有关plugin.yml文件的官方文档在这里: 20 | > https://www.spigotmc.org/wiki/plugin-yml/ 21 | 22 | ## 必要设置项 23 | `plugin.yml`文件中, `name`、`main`、`version`三项必须存在. 24 | *这也意味着, 前面的实例中, 我们使用的`plugin.yml`文件, 删去`author`键仍可被服务端正常加载.* 25 | 26 | 不妨来认识一下这三个设置项. 27 | 28 | ### name 29 | 顾名思义, 它定义了插件的名称. 30 | 31 | 对于名称, 官方WIKI中给出了严格的要求, 即只能由 **英文小写或大写字符、阿拉伯数字或下划线** 构成. 决不能出现中文字符、空格等. 32 | 在后续生成插件配置文件夹时, 该项设置的插件名将会是插件配置文件夹的名称. 33 | 34 | 起名的时候应该注意, 尽可能起一个“个性”的名称, 防止与其他插件重名. 35 | 36 | ### version 37 | 指插件的版本号. 38 | 该键理论上可以在后面填写任意String内容. 但是官方WIKI要求尽可能使用X.X.X格式的版本号表示(例如: 2.3.3). 39 | 关于版本号规则,可以参考[语义化版本](https://semver.org/lang/zh-CN/) 40 | 41 | ### main 42 | 指插件的主类名. 43 | 44 | 在插件中, 主类有且只有一个, 且需要继承`JavaPlugin`类. 主类是插件的“入口”, 这里的`main`即意在说明主类的名称. 45 | 这里需填写主类的全名, 也就是精确到主类所在的具体包. 说白了就是不只是需要把主类名带上, 还要把包名带上. 46 | 47 | ## 可选设置项 48 | `plugin.yml`文件只需要存在必要设置项的三个键即可. 49 | 下面的键可选, 可有可无. 但有一些在一些特定的情况下必须要有. 50 | 51 | ### 依赖 52 | 有时候你的插件可能需要调用`Vault`(用来获取玩家货币余额)或其他的插件, 即依赖其他插件. 53 | 这时候需要在`plugin.yml`文件中进行设置告知服务端, 从而保证所依赖的插件在本插件之前被加载. 54 | 55 | 你可以在`plugin.yml`文件中加入`depend`键或`softdepend`键来控制依赖. 56 | 57 | `depend`键或`softdepend`键接的值必须是数组. 例如这样: 58 | ```yml 59 | depend: [Vault, WorldEdit] 60 | softdepend: [Essentials] 61 | ``` 62 | 两个键设置的内容区别如下: 63 | 1. depend: 插件强制要求的依赖. 如果没有这个插件, 该插件将无法正常工作, Bukkit此时会抛出相应错误. 64 | 2. softdepend: 插件不强制要求的插件. 如果服务端内没有这个插件, 插件仍可正常工作. 65 | 66 | 后面设置的数组内的内容都是所依赖插件的名称, 此处名称应与所依赖的插件的`plugin.yml`文件的`name`键的值相同. 67 | 68 | ### loadbefore 69 | `depend`与`softdepend`可以实现插件在某个插件之后加载. 但也许有时你的插件可能需要实现在某个插件之前被加载. 70 | 此时你可以使用`loadbefore`设置, 用法类似. 例如: 71 | ```yml 72 | loadbefore: [Essentials, WorldEdit] 73 | ``` 74 | 75 | 在上面的例子中, 可保证插件在WorldEdit与Essentials插件之前被加载. 76 | 77 | ### commands 78 | 如果你的插件定义了新指令, 你第一步就需要设置该项告知服务端. 79 | 此处仅做示范: 80 | ```yml 81 | commands: 82 | test: 83 | description: "Hello World!" 84 | ``` 85 | 这可以告知服务端注册了指令`test`, 并且描述为`Hello World!`字符串, 该描述字符串将会在`/help`指令中被显示. 86 | 87 | ### author与authors 88 | 此处不再赘述其作用. 如果你想表示多名作者, 你可以设置`authors`项, 值需为一个数组. 89 | ```yml 90 | authors: [tdiant, Seraph_JACK] 91 | ``` 92 | 如果同时存在`author`与`authors`, 将忽略`author`. 93 | -------------------------------------------------------------------------------- /unit/3-4.md: -------------------------------------------------------------------------------- 1 | # 配置API的序列化和遍历 2 | 3 | # 序列化 4 | ## 了解序列化 5 | 如果我自己做了一个类型, 例如下面的`Person`类: 6 | 7 | ```java 8 | public class Person { 9 | public String name; 10 | public String introduction; 11 | 12 | public Person(String name, String introduction) { 13 | this.name = name; 14 | this.introduction = introduction; 15 | } 16 | } 17 | ``` 18 | 19 | 现在我们新建一个`Person`对象: 20 | 21 | ```java 22 | Person person = new Person("tdiant", "hello!!"); 23 | ``` 24 | 25 | 我们想把`person`保存在配置文件里怎么办? 26 | 很遗憾,直接`getConfig().set("demo",person);`是行不通的. 你会发现`getConfig.get("demo")`根本得不到这个对象. 27 | 28 | > 哪些东西可以直接set保存呢? 29 | > 类似getInt, 所有拥有get方法的类型都可以直接保存. (包括`List`) 30 | > 31 | > 还有一些BukkitAPI给的类型, 例如ItemStack. 但不是全部都是这样. 32 | > 如果你想判断一个类型是不是可以直接set, 你可以在JavaDoc中找到它, 看它是否实现了`ConfigurationSerializable`类. 33 | 34 | 35 | 你可能想到了最简单粗暴的办法: 36 | ```java 37 | //这样set 38 | getConfig().set("demo.name",test.name); 39 | getConfig().set("demo.introduction",test.introduction); 40 | //然后保存, 用的时候这样 41 | getConfig().getString("demo.name"); 42 | getConfig().getString("demo.introduction"); 43 | ``` 44 | 45 | 这的确是一种切实可行的办法. 但是这真的是太麻烦了. 有没有一种方法直接set或get这个对象的办法呢? 有! 你可以使用序列化和反序列化实现它! 46 | 47 | ## 让自定义类型实现序列化与反序列化 48 | 以上文`Person`为例. 首先让他实现`ConfigurationSerializable`, 并添加`deserialize`方法. 如下: 49 | ```java 50 | public class Person implements ConfigurationSerializable { 51 | public String name; 52 | public String introduction; 53 | 54 | public Person(String name, String introduction) { 55 | this.name = name; 56 | this.introduction = introduction; 57 | } 58 | 59 | @Override 60 | public Map serialize() { 61 | Map map = new HashMap<>(); 62 | return map; 63 | } 64 | 65 | public static Person deserialize(Map map) { 66 | 67 | } 68 | } 69 | ``` 70 | 71 | 然后继续完善`serialize`, 实现序列化. 我们只需要把需要保存的数据写入map当中即可. 72 | 注意, 需要保存的数据要保证可以直接set, 不能则也需要为他实现序列化与反序列化. 73 | ```java 74 | @Override 75 | public Map serialize() { 76 | Map map = new HashMap<>(); 77 | map.put("name",name); 78 | map.put("introduction",introduction); 79 | return map; 80 | } 81 | ``` 82 | 83 | 序列化后, 数据即可直接set进配置文件里. 为了实现直接get的目的, 还需要进行反序列化. 84 | ```java 85 | public static Person deserialize(Map map) { 86 | return new Person( 87 | (map.get("name") != null ? (String) map.get("name") : ""), 88 | (map.get("introduction") != null ? (String) map.get("introduction") : "") 89 | ); 90 | } 91 | ``` 92 | 编写完毕后, 我们需要像注册监听器一样, 注册序列化. 在插件主类的`onEnable`中加入如下语句: 93 | ```java 94 | ConfigurationSerialization.registerClass(Person.class); 95 | ``` 96 | 至此, 你就可以自由地对一个自定义的对象直接地get和set了! 97 | 下面分别演示set与get: 98 | ```java 99 | // set 100 | Person person = new Person("tdiant", "hello!!"); 101 | getConfig().set("demo", person); 102 | saveConfig(); 103 | ``` 104 | 默认配置文件(config.yml)将出现: 105 | ```yml 106 | demo: 107 | ==: myplugin.Person 108 | name: tdiant 109 | introduction: hello!! 110 | ``` 111 | BukkitAPI会根据`==`的值判断这段配置是由什么类序列化而来的, 进而方便其反序列化. 112 | 不要轻易改动`==`属性的值, 否则BukkitAPI会因为找不到类或此类没有被注册(ConfigurationSerialization类的registerClass方法)而报错. 113 | ```java 114 | // get 115 | Person person = (Person) getConfig().get("demo"); 116 | System.out.println("name = " + person.name + " introduction = " + person.introduction); 117 | ``` 118 | 控制台将输出: 119 | ``` 120 | name = tdiant introduction = hello!! 121 | ``` 122 | # 配置文件的遍历 123 | 试想, 如果存在下面的配置文件: 124 | ```yml 125 | demo_list: 126 | a: 1 127 | b: 233 128 | c: 666 129 | d: lalalalalal 130 | ``` 131 | 我应该如何对`demo_list`的子键进行遍历, 得到所有子键的对应值? 132 | 最简单错报的方式就是将`demo_list.a`键、`demo_list.b`键...依次读取. 但这是建立在你知道`demo_list`有`a`、`b`、`c`...这些子键的基础之上的. 133 | 如果我事先不知道`demo_list`的子键都各自叫什么, 又应该如何对`demo_list`的子键进行遍历, 得到所有子键的对应值? 134 | 135 | ## 配置片段 ConfigurationSection 136 | 我们可以把`demo_list`键对应的部分拆出来. 137 | *下文假设config对象是我们现在正在操作的FileConfiguration对象.* 138 | ```java 139 | ConfigurationSection cs = config.getConfigurationSection("demo_list"); 140 | ``` 141 | 这里我们得到了`ConfigurationSection`对象, 这个对象可以当做config对象`demo_list`键部分的片段, 等效于这个yaml数据: 142 | ```yml 143 | a: 1 144 | b: 233 145 | c: 666 146 | d: lalalalalal 147 | ``` 148 | 对于一个`ConfigurationSection`对象, 其代表着一个完整配置数据的某个片段, 你不能直接利用诸如`saveConfig`的方式保存这个片段到另外一个yml文件里. 149 | 150 | ## 利用getKeys方法实现遍历 151 | 在上面我们得到了`ConfigurationSection`对象, 这代表着config对象`demo_list`键部分的片段. 152 | 现在问题转化成了, 如何获取到`ConfigurationSection`对象里的所有键. 153 | 可以利用`getKeys(false)`的方式达到目的. 154 | 155 | ```java 156 | for(String key : cs.getKeys(false)) { 157 | System.out.println(key + " = " + cs.get(key)); 158 | } 159 | ``` 160 | 上面的代码将输出: 161 | ``` 162 | a = 1 163 | b = 233 164 | c = 666 165 | d = lalalalalal 166 | ``` 167 | 这样就实现了遍历. 168 | 169 | `getKeys`方法不只是`ConfigurationSection`拥有, 根据其继承关系, 我们可以推知对`FileConfiguration`类也拥有`getKeys`方法, 同理, `ConfigurationSection`类也有`getConfigurationSection`方法. 170 | 171 | 但是我们刚才为什么要给`getKeys`的一个`false`的参数呢? 请看下面的yaml数据: 172 | ```yml 173 | test: 174 | a: 175 | b: 1 176 | c: 2 177 | d: 1 178 | ``` 179 | 我们得到了这个配置文件的`FileConfiguration`对象`config`, 现在对其用`getKeys(false)`进行遍历, 得到所有键. 180 | ```java 181 | for(String key : config.getKeys(false)) { 182 | System.out.println(key); 183 | } 184 | System.out.println("==================="); 185 | for(String key : config.getKeys(true)) { 186 | System.out.println(key); 187 | } 188 | ``` 189 | 输出结果如下: 190 | ``` 191 | test 192 | d 193 | =================== 194 | test 195 | test.a 196 | test.a.b 197 | test.c 198 | d 199 | ``` 200 | 由此可知, `getKeys(false)`只能获取“一层的键”, 不能递归获取配置文件里所有的键. 而`getKeys(true)`会递归获取配置文件里所有出现的键. 201 | -------------------------------------------------------------------------------- /unit/3-5.md: -------------------------------------------------------------------------------- 1 | # Bukkit 的多线程多任务框架 2 | 3 | # 前言 4 | 本节前半部分内容基本是对Javadoc的复述, 以及使用它们的注意事项. 如果此前您已经使用过了此包, 或者您有良好的文档阅读及应用能力, 建议您先阅读“注意事项”和“小技巧”一栏, 这才是本节教程更重要的知识! 5 | 6 | # org.bukkit.scheduler 包结构 7 | Bukkit 的多线程多任务框架放在了此包, 此包只含有三个接口(`BukkitSheduler`, `BukkitTask`, `BukkitWorker`)和一个抽象类(`BukkitRunnable`,实现了java.lang.Runnable). 相关实现在实现了 Bukkit API 的底层服务器代码中(比如CraftBukkit). 8 | 他们之间的关系大致是这样的: `BukkitSheduler` 负责调度/创建任务,并管理他们(类似于线程池). `BukkitTask` 负责存储由 `BukkitSheduler` 调度的单个任务, 并提供获取它们的任务 id 以及取消它们的一系列方法. `BukkitWorker` 是处理对应异步任务的worker线程. `BukkitRunnable` 基本上是对 BukkitScheduler 的包装, 使用它比使用 BukkitScheduler 相对来说更简洁些. 9 | 10 | # 访问 org.bukkit.scheduler 的两个入口 11 | 一是使用`org.bukkit.Bukkit.getScheduler()`或`org.bukkit.Bukkit.getServer().getScheduler()`获取`BukkitScheduler`实例. 12 | 例子: 13 | ```java 14 | Bukkit.getScheduler().runTask(this, new Runnable() { 15 | @Override 16 | public void run() { 17 | // 逻辑代码 18 | } 19 | }); 20 | ``` 21 | 另一个是构造一个继承`org.bukkit.scheduler.BukkitRunnable`的匿名内部类, 就像这样: 22 | ```java 23 | new BukkitRunnable() { 24 | @Override 25 | public void run() { 26 | // 您的代码逻辑 27 | } 28 | }.runxxx(); 29 | ``` 30 | 然后再调用 BukkitRunnable 里的各种方法(事实上最终它还是要访问`BukkitScheduler`, 因此两种方法是等效的). 您也可以直接在Runnable内调用BukkitRunnable的方法, 实现自我取消, 等等. 使用BukkitRunnable的优点在于它简单便捷. 31 | 32 | # 如何使用 33 | 在这里只介绍Bukkit 任务调度API的核心 ———— BukkitScheduler 的使用方法, 并且不对那些已过时的方法做解释说明(通常情况下你不应该使用它们). 34 | 值得注意的是, Bukkit 的调度任务系统是以 Minecraft 的游戏刻为时间单位的, 其中一个游戏刻(又叫做tick, 下文都使用`tick`指代游戏刻)对应现实世界的50ms(也就是说, 理想情况下20 ticks是一秒). 但实际上受服务器性能因素的影响, 不一定每一tick都精确地经过了50ms (服务器每秒经过的ticks数可以使用命令`tps`查询). 所以在您编写Bukkit 插件时, 请把你置身于 Minecraft 的世界里:) 35 | 如果没有特别说明, Bukkit所提供的调度任务的方法, 时间均以tick为单位. 方法全名规则是前者为方法返回值, 后者为方法名和相关参数. 36 | 37 | ## 调度同步任务 38 | ### BukkitTask runTask(Plugin plugin, java.lang.Runnable task) 39 | 这是调度**同步任务**的主要方法, 另一个方法`runTaskLater`提供了一个`delay`延迟参数, 用于指定调度任务多久后才开始执行. 不指定`delay`的情况下, delay值为1. 40 | ### BukkitTask runTaskTimer(Plugin plugin, java.lang.Runnable task, long delay, long period) 41 | 这是调度重复任务的方法, 所得的任务是**同步**的, `period`最低值为1,您不能将其设为比1低的值 (若设为0则等效于1, 小于0表示该任务不是重复的). 42 | 由于是同步任务, 您在Runnable的run()方法中的代码, 是运行于服务器主线程的, 所以请仔细评估这些代码的效率, 因为这可能会影响服务器的性能(尤其是TPS指标), 从而降低服务器流畅度. 如果不与 Minecraft 有关, 请放在下面要介绍的异步任务. 43 | 44 | ## 调度异步任务 45 | ### BukkitTask runTaskAsynchronously(Plugin plugin, java.lang.Runnable task) 46 | 这是调度**异步任务**的主要方法, 另一个方法`runTaskLaterAsynchronously`提供一个`delay`延迟参数. 47 | ### BukkitTask runTaskTimerAsynchronously(Plugin plugin, java.lang.Runnable task, long delay, long period) 48 | 这是调度重复任务的方法, 所得的任务是**异步**的. 通常我们使用异步任务来处理非Minecraft的逻辑,比如数据库的CRUD(增删改查)操作. 49 | 在异步任务中, 需要特别注意线程安全问题, 比如您不能随意调用 Bukkit API. 这个问题会稍后予以详细的解释说明. 50 | 51 | # 注意事项 52 | ## 线程安全 53 | Bukkit API文档清楚地告诉我们异步任务中不应访问某些Bukkit API, 需要着重考虑线程安全. 大多数 Bukkit API 不是线程安全的. 54 | 什么是线程安全呢? 55 | > 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。 56 | > “引自百度百科” 57 | 58 | 大多数集合不是线程安全的, 比如经常使用的`HashMap`、`ArrayList`. 同样适用于非线程安全的对象. 59 | 限于篇幅, 这里不作深入探讨. 想要了解更多, 请询问您的书籍与搜索引擎. 60 | Bukkit 中的线程安全? 61 | Minecraft 中几乎所有的游戏逻辑都运行于主线程中, 而插件的大多数逻辑也是运行于主线程中的, 这包括插件命令的执行、(同步)事件的处理等等. 62 | 如果我们调度了一个异步任务, 或者处于异步事件中, 那么就不应当访问与Minecraft游戏内容有关的API(比如操作方块、加载区块、踢出玩家等). 尝试这么做极有可能得到异常, 使得插件崩溃. 63 | 64 | ## 如何在异步任务中调度同步任务, 以访问 Bukkit 的非线程安全的方法? 65 | 一种就是`BukkitScheduler.runTask` (方法不带`asynchronously`字眼). 这返回的永远是同步任务, 可以大胆访问 Bukkit API, 就像这样: 66 | ```java 67 | Bukkit.getScheduler().runTaskAsynchronously(this, () -> { 68 | // 从数据库拉取些数据 69 | // 执行同步任务 70 | Bukkit.getScheduler().runTask(ExamplePlugin.instance, () -> player.sendMessage("你好, 世界!")); 71 | }); 72 | ``` 73 | 另一种就是`BukkitScheduler.callSyncMethod`, 这个会在之后的小技巧一栏作介绍. 74 | ## Bukkit API中哪些操作是非线程安全的, 哪些又是线程安全的? 75 | > 不完整列表. 仅供参考. 不保证线程安全的方法的行为将来会变化. 不对版本差异导致的行为不同作担保. 76 | 77 | 线程安全的有: 78 | 1. scheduler包自身. 79 | 2. Player#sendMessage() 80 | > 你可以发现大量插件在AsyncPlayerChatEvent事件中调用player.sendMessage(). 因此我们有理由确信这是线程安全的. 81 | 3. PluginManager#callEvent(event) 82 | > 用于触发事件的方法. 在`SimplePluginManager`中, 该方法使用了synchronized关键字对其实例加锁, 因此是线程安全的. 更多细节请阅读源代码. 83 | 4. 发包 - sendPacket 84 | > 为何Player#sendMessage()是线程安全的就是因为它. 我们可以深入craftbukkit乃至nms(net.minecraft.server), sendPacket不过是将数据包传入netty管道, 让netty处理. 如果某个方法仅仅执行了发包流程而没有实际从游戏里加载数据, 那么一般可视其为线程安全的. 因此利用`World#spawnParticle`发送粒子效果以及`World#playEffect`向玩家发送特效、`Player#sendTitle`向玩家发title等也是线程安全的. 我们可以把相关数学运算放到异步线程中, 算完再切换线程发粒子特效. 85 | 86 | 非线程安全的有: 87 | 1. 设置/获取方块、加载/生成区块 88 | 2. 操作实体 89 | 3. 权限检查(是的. 某些情况下这是非线程安全的, 因为插件一同共享权限列表) 90 | 91 | ## 关闭插件时, 确保取消你调度的所有任务 92 | 最简单的方法就是在插件主类的`onDisable`方法写上这一行代码: 93 | ```java 94 | Bukkit.getScheduler().cancelTasks(plugin); 95 | ``` 96 | 其中plugin是你的插件实例, 通常是`this`. 97 | 如果不这么做,那么你的插件被关闭之后, 残存的任务(一般是重复任务)仍在运行, 任务会调用相关变量, 而你在关闭插件时如果清理了那些变量, 将会导致一些无法预料的问题. 98 | 99 | # 小技巧 100 | ## 使用 lambda 表达式替换匿名内部类 101 | 自Java 8开始提供对 lambda 表达式的支持. 匿名内部类转 lambda 表达式可使代码看上去更加简洁漂亮. 比如 102 | ```java 103 | scheduler.runTask(this, new Runnable() { 104 | @Override 105 | public void run() { 106 | System.out.println("这是从在任务中输出的一句话."); 107 | } 108 | }); 109 | ``` 110 | 可以替换成: 111 | ```java 112 | scheduler.runTask(this, () -> System.out.println("这是从在任务中输出的一句话.")); 113 | ``` 114 | 是不是觉得匿名内部类多不优雅, 而 lambda 表达式一行就解决了所有问题? 尽早对丑陋的匿名内部类说byebye吧~ 115 | 116 | ## 使用 BukkitScheduler 提供的`callSyncMethod`方法 117 | > 其实这不应出现在这里的. 不过使用这种方法有点门槛, 如果没有学过相关概念, 你可能不知道从何下手. 该方法涉及到了 Java 的 Future 和 Callable 概念. 如果不知道是什么, 可以搜索来查找资料. 相对于线程安全, Future 和 Callable 概念理解起来容易多了. 118 | 119 | 这也是使你的代码置于服务器主线程执行的方法之一, 通常用于需要在主线程执行操作获取数据并返回给异步线程的场景. 120 | 下面是鄙人对这些概念的粗略理解: 121 | > 常规的Runnable的run方法是没有返回值的, 它是一个void方法. 这时我们需要使用`Callable`, `Callable`的call方法是有返回值的, 值类型受泛型影响. 使用Runnable还有一个缺点:我(Boss)命令手下一位职员做点任务. 命令完后(开线程, 使用Runnable), 我需要等待职员做完任务的一些反馈, 没有职员提供的数据不能继续工作. 然后在职员执行完任务之前我能干嘛? 没办法, 只能等, 无论职员会执行多久. 有没有办法, 在职员执行任务的过程中, 我还可以做点别的事情呢? 122 | 123 | Java提供了Future这个模式. 于是上面的情况变成了这样: 124 | > 我命令手下一位职员做点任务. 命令完后(开线程, task为FutureTask), 我可以做些别的事情了, 比如与某某打情骂俏...... 之后我可以询问那位职员事情做完没有(Future#isDone()), 或者直接问他结果(Future#get()), 这个取值过程是阻塞的, 直到那位职员完成任务后才能报告结果. 如果我等不耐烦了我还可以使他停下来, 不做了(Future#cancel(boolean)). ~~甚至看不顺眼解雇他~~ 等待职员完成任务的同时, 又多了一份愉悦, 何乐而不为呢~ 125 | 126 | 这里就不作更多介绍了. 欲了解更多内容和用法可以参考[Javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html) 以及询问搜索引擎. 127 | 128 | 直接上食用方法吧! 这是一个使用主线程获取当前在线玩家数量并返回的例子: 129 | ```java 130 | Future future = Bukkit.getScheduler().callSyncMethod(ExamplePlugin.instance, () -> { 131 | // call方法是可以抛出异常的 132 | // 假设这个操作有些耗时...这是对主线程的sleep(事实上这最好不要超过50ms) 133 | Thread.sleep(1000); 134 | return Bukkit.getOnlinePlayers().size(); 135 | }); 136 | try { 137 | // 比如这里是数据库操作过程, 假设连接数据库并进行操作耗时1s, 这时我们应该可以拿到在线玩家数了 138 | // 如果操作过程小于1s更好, 只要等上面的方法执行完即可 139 | // future.get()是阻塞的, 直到执行完毕 140 | int players = future.get(); 141 | // 向数据库写入数据 142 | System.out.println("玩家数:" + players); 143 | } catch (InterruptedException | ExecutionException e) { 144 | // 异常处理 145 | } 146 | ``` 147 | 这段代码是在异步任务中运行的. 148 | 149 | 食用方法可以说是较复杂了, 如果你没有获取数据的需要, 仅仅需要在主线程内运行特定代码, 使用`BukkitScheduler#runTask()`更好. 没有必要为了 bigger 而 bigger, 唯有**simple**得人心. 150 | -------------------------------------------------------------------------------- /unit/3-6.md: -------------------------------------------------------------------------------- 1 | # 自定义合成表 2 | 3 | 在背包、工作台中, 玩家可以通过指定的物品摆放, 消耗所摆放的物品得到新物品, 这被称作物品的合成. 物品的摆放方式与得到的新物品即为合成表. 4 | 5 | ## 合成表物品摆放的文字表述法 6 | 7 | 如何用文字表述物品在2X2格子或3X3格子中的摆放方式? 8 | 9 | 首先我们来使用数学中“方程”的概念, 把金锭设成x, 把铁锭设成y. 打个比方, 我现在想实现八个金锭和一个铁锭合成一个绿宝石, 摆放方式可以这样表示: 10 | 11 | ``` 12 | xxx 13 | xyx 14 | xxx 15 | ``` 16 | 17 | 那现在如果想表示类似“工作台”的合成方式呢? 工作台合成需要四个木板, 在背包的合成区内可以填满木板来合成, 在工作台合成区内可以有这样的摆放方式: 18 | 19 | ``` 20 | 设x为木板 21 | xx空 22 | xx空 23 | 空空空 24 | 25 | 空xx 26 | 空xx 27 | 空空空 28 | 29 | 空空空 30 | xx空 31 | xx空 32 | 33 | 空空空 34 | 空xx 35 | 空xx 36 | ``` 37 | 对于这样的非3X3的合成方式, 我们可以这样表示: 38 | 39 | ``` 40 | xx 41 | xx 42 | ``` 43 | 44 | 这也意味着, 在用文字表述合成表时, 不一定非得是3x3的表示方式, 还可以2x2, 还可以1x1, 只要是mXn的格式即可(例如门的合成是2X3). 45 | 46 | ## 新建合成规则 47 | 48 | 以上文合成金铁锭合成绿宝石为例, 在onEnable方法的适当位置添加如下内容: 49 | 50 | ```java 51 | ShapedRecipe sr1 = new ShapedRecipe( 52 | new ItemStack(Material.EMERALD)) //合成出的物品(提示: 修改这个ItemStack的Amount可以控制能合成多少个目标物品) 53 | .shape("xxx","xyx","xxx") //这是刚才我们摆出来的文字表述 54 | .setIngredient('x',Material.GOLD_INGOT). //设x为金锭 55 | setIngredient('y',Material.IRON_INGOT); //设y为铁锭 56 | 57 | getServer().addRecipe(sr1); 58 | ``` 59 | 60 | 在onDisable方法中添加如下内容: 61 | 62 | ```java 63 | getServer().clearRecipes(); 64 | ``` 65 | 66 | 经验证可发现, 现在我们可以通过控制台通过在铁锭周围围一圈金锭的方式合成绿宝石了. 67 | 68 | 那么设x为金锭, 我想实现像熔炉那样, 八个金锭围一圈合成一个绿宝石, 不需要铁锭了. 就像这样: 69 | 70 | ``` 71 | xxx 72 | x空x 73 | xxx 74 | ``` 75 | 76 | 中间有个位置是空的, 该怎么办? 应该设个y表示AIR吗? 不需要, 空位置可以使用空格表示. 就像下面这个例子: 77 | 78 | ```java 79 | ShapedRecipe sr2 = new ShapedRecipe( 80 | new ItemStack(Material.EMERALD)) //合成出的物品 81 | .shape("xxx","x x","xxx") //这是刚才我们摆出来的文字表述(中间的y改成了空格) 82 | .setIngredient('x',Material.GOLD_INGOT). //设x为金锭 83 | setIngredient('y',Material.IRON_INGOT); //设y为铁锭 84 | 85 | getServer().addRecipe(sr2); 86 | ``` 87 | 88 | shape方法的参数个数不限制, 这也意味着你可以这样表述非3X3摆放方式: 89 | 90 | ```java 91 | .shape("x") //1X1(就像按钮那样的摆放方式) 92 | .shape("xx","xx","xx") //2X3(就像木门那样的摆放方式) 93 | .shape("xx","xx") //2X2(就像合成台那样的摆放方式) 94 | ``` 95 | 96 | 如果你这样设置 97 | 98 | ```java 99 | .shape("xx ","xx "," ") 100 | ``` 101 | 102 | 那玩家在游戏中只能这样在合成台合成: 103 | ``` 104 | xx空 105 | xx空 106 | 空空空 107 | ``` 108 | 109 | 而不能用其他等效的位置摆放合成, 比如这样: 110 | 111 | ``` 112 | 空空空 113 | xx空 114 | xx空 115 | ``` 116 | -------------------------------------------------------------------------------- /unit/3-7-math.md: -------------------------------------------------------------------------------- 1 | # 几何初步 2 | 3 | 几何基础知识是做特效的基础内容,应当了解。 4 | **作者高中数学没及过格,这里内容仅供参考。** 5 | **若您了解相关内容或接受了高中数学的有关学习,您可以跳过这部分内容。** 6 | *不建议跳过这部分内容。* 7 | 8 | # 坐标系 9 | Minecraft是一款3D游戏。对于任意一个方块,都可以用X、Y、Z来表示他的位置。 10 | 11 | Minecraft采用的是右手坐标系。 12 | 试试看,如果可以的话,伸出右手,中指指向自己,食指指向天,大拇指指向你的右方. 把大拇指指向作为X轴的正方向,中指指向作为Z轴的正方向,食指指向作为Y轴的正方向,那么你可以建立这样的坐标系: 13 | 14 | ![](https://i.loli.net/2019/07/10/5d25616d9cab049683.jpg) 15 | 16 | *图源自网络. 我懒得画图了...* 17 | 18 | # 平面直角坐标系中的三角函数 19 | ## 概念 20 | 我们暂时只画X轴和Y轴,即平面直角坐标系. 这是我们在初中阶段所熟知的坐标系. 21 | 22 | ![](https://i.loli.net/2019/07/10/5d2562dfc0baa82701.jpg) 23 | 24 | 对于上图中的∠α, 根据初中我们所知的三角函数定义,我们可以知道: 25 | ``` 26 | α的正弦值 sinα=MP/OP=b/r 27 | α的余弦值 cosα=OM/OP=a/r 28 | α的正切值 tanα=MP/OM=b/a 29 | ``` 30 | 31 | ## 三角函数的诱导公式 32 | 通过上面的方法,此我们可以轻易推算出0至90°内任一角的sin、cos、tan值. 33 | 后续经过大量的事实、数学推导以及无数数学家的研究,三角函数有这些规律性: 34 | 35 | 对于一个整数k,任一角α,我们有: 36 | ``` 37 | 公式一:设α为任意角,终边相同的角的同一三角函数的值相等 38 | sin(2kπ+α)=sinα(k∈Z) 39 | cos(2kπ+α)=cosα(k∈Z) 40 | tan(2kπ+α)=tanα(k∈Z) 41 | 公式二:设α为任意角,π+α的三角函数值与α的三角函数值之间的关系 42 | sin(π+α)=-sinα 43 | cos(π+α)=-cosα 44 | tan(π+α)=tanα 45 | 公式三:任意角α与-α的三角函数值之间的关系 46 | sin(-α)=-sinα 47 | cos(-α)=cosα 48 | tan(-α)=-tanα 49 | 公式四:利用公式二和公式三可以得到π-α与α的三角函数值之间的关系 50 | sin(π-α)=sinα 51 | cos(π-α)=-cosα 52 | tan(π-α)=-tanα 53 | 54 | 以及其他一些公式 55 | ``` 56 | 57 | ## 任意角的三角函数 58 | 利用这些公式我们可以推算出,对于任一角的sin、cos、tan值为多少. 59 | 60 | 这些是常用的三角函数值,在高中阶段应当熟练背会. 61 | ![](https://i.loli.net/2019/07/12/5d28a08b1240b19931.jpg) 62 | 63 | ## 三角函数的规律性周期变化 64 | 65 | 通过上面的分析,我们可以使用画图的方法,得到三角函数的图像,并且得知三角函数是周期性函数: 66 | 67 | ![](https://i.loli.net/2019/07/12/5d28a1205777565750.jpg) 68 | -------------------------------------------------------------------------------- /unit/3-7.md: -------------------------------------------------------------------------------- 1 | # 粒子效果及音效播放 2 | 3 | [**阅读本文需要高中数学的几何基本知识**](3-7-math.md) 4 | 5 | ## 粒子效果 6 | 7 | 客户端正常配置时,若对草方块上使用骨粉,草方块上会长出草丛,同时还会生成绿色的颗粒动画. 这样的动画效果就是Minecraft中的粒子效果. 8 | 9 | ### 播放粒子效果 10 | 如果想在某一个`Location`对象所对应的位置播放粒子效果,对于不同的Minecraft版本有不同的方案: 11 | 12 | #### PlayEffect 13 | 可以利用World类的`PlayEffect`方法: 14 | *对于Effect,BukkitAPI在后续的更改中,其中的枚举几乎都或多或少有些许改动。开发时应小心。* 15 | 16 | ```java 17 | Location loc = 某一Location对象; 18 | loc.getWorld.playEffect(loc, Effect.HAPPY_VILLAGER, 1); //播放的是绿色的闪光星星⭐效果 19 | ``` 20 | 21 | `PlayEffect`方法在较早的BukkitAPI版本中即被加入. 在使用这一方法时需要与`Effect`打交道. 22 | `Effect`是效果枚举. 值得注意的是,这其中既包含动效(Effect.Type.VISUAL),也包含声效(Effect.Type.SOUND). 23 | 24 | ***作为一个老旧的API,在实际开发当中,这一方法并不常用. 其中的常见枚举(例如这里使用的HAPPY_VILLAGER)在新的API中被标记废除.*** 25 | 26 | #### spawnParticle 27 | 在新版的API中加入了`spawnParticle`方法. 目前开发插件常用这一方法来播放粒子效果. 28 | 29 | 新版的BukkitAPI有意将`Sound`与`Visual`这两个概念分隔开,对于粒子效果,在使用`spawnParticle`方法时,取`Effect`而代之的是`Particle`枚举. 30 | 31 | *spawnParticle的用法较多,在此略去大篇幅对各个方法与参数的介绍,可以查阅JavaDoc,其中有十分简单易懂的注释.* 32 | *BukkitAPI后续更新中,枚举或多或少都有变动,应当注意!* 33 | 34 | ### 播放所需的形状 35 | 36 | > 开发实例: 在玩家脚底播放一圈半径为1的粒子效果 37 | 38 | **分析** 39 | 1. 几何角度考虑 40 | 41 | 以玩家脚底处为原点,建立平面直角坐标系. 如下图所示: 42 | ![](https://i.loli.net/2019/07/12/5d2893acab56b12879.jpg) 43 | 44 | *绿色部分为粒子效果* 45 | 46 | 由圆的定义知,所绘制的粒子为到原点的点集. 47 | 48 | 2. 实现 49 | 播放想要的形状就是逐次的在所需播放的坐标处播放粒子效果. 50 | 51 | *这里将不解释什么是弧度制,而是做强制要求,只要算角度都必须用这样的方法变为弧度制,有兴趣可以在网上查阅* 52 | 53 | ```java 54 | Location loc = p.getLocation().clone(); 55 | for(int t=0;t<360;t++){ //这里的t表示旋转角,从0到360度遍历一遍就是转了一圈 56 | double r = Math.toRadians(t); //角度制变弧度制 57 | //在这里,我们使用三角函数依次计算出了对应点的坐标. 58 | //建议作图体会这样计算的原理. 59 | double x = Math.cos(r); 60 | double y = Math.sin(r); 61 | //在刚开始时,loc是坐标系原点(也就是玩家所在的位置) 62 | //这里我们的add将其变为了我们想要播放粒子的坐标位置 63 | //后面我们又subtract(减)将其又变为了坐标原点 64 | loc.add(x,0,y); 65 | loc.getWorld().spawnParticle(Particle.VILLAGER_HAPPY,loc,1,null); 66 | loc.subtract(x, 0, y); 67 | } 68 | ``` 69 | 70 | 这样我们就完成了这一效果. 71 | 72 | 依此,可以大致概括出实现粒子效果的基本步骤: 73 | 1. 分析: 从数学角度分析, 思考怎么才能获得所需形状中所有的点;从代码角度分析,思考怎样才能依此获得这些点的坐标值 74 | 2. 实现:利用恰当的方法播放粒子效果 75 | 76 | ## 音效播放 77 | 由于`Effect`既包含动效,也包含声效,这意味着使用与上面`PlayerEffect`方法一样的方法,我们也可以播放音效. 78 | 79 | 在新API中提供了`playSound`方法并且加入了`Sound`枚举. 目前常用这一方法. 这一方法是World也同样是Player类的方法, 具体使用哪一方法,取决于你希望对谁播放. 80 | 81 | *BukkitAPI后续更新中,枚举或多或少都有变动,应当注意!* 82 | -------------------------------------------------------------------------------- /unit/3-8.md: -------------------------------------------------------------------------------- 1 | # 世界生成器 2 | 3 | > 本章编写参考了如下内容, 这篇文章对于插件开发而言十分重要: 4 | > https://www.mcbbs.net/thread-811614-1-1.html 5 | 6 | > [如果你对Minecraft 1.13中世界生成机制大改动感兴趣, 可以点击这里.](https://www.mcbbs.net/thread-846195-1-1.html) 7 | > 并且, [对于Minecraft 1.13之前版本的世界生成阐述, 可以见此文](https://www.zhihu.com/question/20754279) 8 | > 9 | > *在Bukkit中, 截止到目前, BukkitAPI仍沿用旧有规则的API.* 10 | > *这意味着本文内容截止目前对于新版本的插件开发仍然有效.* 11 | 12 | 本文中使用了`Material.GRASS_BLOCK`, 这是1.13版本的新用法. 13 | 在旧版API中, 应该使用`Material.GRASS`. 14 | 15 | ## 简述世界生成 16 | 17 | Minecraft中, 一个世界(World)按照一定的大小被分为多个区块(Chunk). 18 | MC会自动地按照一定的规则卸载无人Chunk, 在需要的时候加载所需的Chunk到内存, 以此来保证一个World被加载到内存, 这样不至于整个World都需要加载到内存以备调用. 19 | 世界的生成同样以Chunk为单位. 20 | 21 | Minecraft游戏中, 世界生成分为两个阶段, 分别为 Generation 与 Population. 22 | 23 | Minecraft生成一个World, 首先进入 Generation 阶段. 这一阶段主要是绘制地形等. 24 | 1. Minecraft首先会获取该Chunk中包含的所有生物群系. 然后会根据特定的生物群系绘制基本的地形. 地形的绘制依靠了一些特殊的算法, 游戏通常会以高度63作为水平面, 通过这些特殊算法绘制基本的地形. 绘制完毕后, 整个世界只有空气、水和石头. 25 | 2. 接着会在高度0-5范围内生成基岩, 并逐个对各个生物群系添加特有的方块. 例如, 对平原添加草方块和泥土, 对沙漠添加沙子和沙石等. 26 | 3. 再然后会生成特殊地形. 这里的特殊地形指的是涉及到多个区块的大型地形, 例如规模很大的洞穴、村庄、矿井等. 27 | 4. 最后会进一步处理, 做最后的准备收尾工作, 至此Generation阶段完毕. 28 | 29 | Generation阶段完成, 意味着该世界的整体结构已经定型. 但是这个世界上还缺少“点缀”. 这个世界上仍然没有树、生物、沼泽上的荷叶、水边的甘蔗等. 此时进入 Population 阶段. 30 | 1. 首先会对该世界的实体进行完善, 并生成各种各样的特殊的方块(指的是箱子等方块实体, 这些方块与其它方块相比复杂许多). 31 | 2. 然后会生成小型地形. 比如一些地表小水坑、地表岩浆池、地下地牢等. 32 | 3. 然后会在地下按照一定的规则生成矿物. 33 | 4. 最后增加地面点缀, 生成水边的甘蔗、沼泽上的荷叶、地面大蘑菇和树木等物, 并增加一些生物群系特定物, 生成一些基础生物(比如牛、鸡、羊等). 34 | 35 | 待 Population 阶段结束后, 该Chunk的数据便会存储起来, 显示出来. 36 | 37 | ## 干涉Population 38 | 39 | Bukkit中, 在世界初始化前会触发`WorldInitEvent`事件. 监听该事件, 我们可以对该世界生成的 Population 阶段进行干涉. 40 | 41 | 在下面的案例中, 我们将在Chunk的 Population 阶段, 在世界的草方块上人为的添加许多钻石块(DIAMOND_BLOCK). 42 | 43 | ```java 44 | public class WorldListener implements Listener { 45 | @EventHandler 46 | public void onWorldInit(WorldInitEvent e){ 47 | if(e.getWorld().getName().equals("World")) 48 | e.getWorld().getPopulators().add(new RuaPopulator()); 49 | } 50 | } 51 | 52 | class RuaPopulator extends BlockPopulator { 53 | @Override 54 | public void populate(World w, Random r, Chunk c){ 55 | final int maxn = 16; //一个区块的X或Y范围是0-16 56 | for(int i=0; i<12; i++){ //这里打算一个区块生成12个 57 | int x = r.nextInt(maxn), z = r.nextInt(maxn); 58 | for (int y = 125; y > 0; y--) { 59 | if (c.getBlock(x, y, z).getType() == Material.GRASS_BLOCK && c.getBlock(x, y + 1, z).getType() == Material.AIR) { 60 | c.getBlock(x, y + 1, z).setType(Material.DIAMOND_BLOCK); 61 | break; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | ``` 69 | 70 | 最终效果如下: 71 | 72 | ![](https://i.loli.net/2020/02/08/AkEZ7VuOv8wWjKm.jpg) 73 | 74 | 可以发现, 生成的world中, 按照我们的设定, 在地表草方块上零散的分布了钻石块. 75 | 这说明在Bukkit中, 你可以创建一个BlockPopulator对象, 在世界初始化时添加为某一World的Populator, 依此来干涉Population阶段. 76 | *Bukkit中的Populator只有BlockPopulator一种.* 77 | *但是你可以以此类推, 通过这种方式实现在地面随机生成某种建筑等其他效果.* 78 | 79 | 值得注意的是, 在自定义的Populator中, populate方法的参数中有一个传入的Random对象. 80 | 这是为了让随机数的生成符合World对应的种子. 在需要生成随机数的时候, 应尽可能使用方法参数中的Random对象. 81 | 82 | ## 控制Generation 83 | 84 | 通过控制一个世界的Generation, 我们可以控制世界的大体地形. 85 | 下面我们将在插件加载时, 生成一个新的世界`RuaWorld`, 这个世界是一个超平坦世界, 第一第二层为基岩, 第三层为草方块. 86 | 87 | ```java 88 | public class Main extends JavaPlugin { 89 | public void onEnable(){ 90 | Bukkit.getPluginManager().registerEvents(new WorldListener(),this); 91 | Bukkit.createWorld(new WorldCreator("RuaWorld").generator(new RuaChunkGenerator())); 92 | } 93 | 94 | public void onDisable(){ 95 | // 96 | } 97 | } 98 | 99 | class RuaChunkGenerator extends ChunkGenerator { 100 | @Override 101 | public ChunkData generateChunkData(World w, Random r, int x, int z, BiomeGrid b) { 102 | ChunkData chunkData = createChunkData(w); //创建区块数据 103 | 104 | //下面这行方法调用参数中, 前三个参数代表一个XYZ对, 后面又是一个XYZ对. 105 | //这两个XYZ对是选区的意思, 你可以结合Residence插件圈地、WorldEdit选区的思路思考. 106 | //提醒: 一个Chunk的X、Z取值是0-16, Y取值是0-255. 107 | chunkData.setRegion(0, 0, 0, 16, 2, 16, Material.BEDROCK); //填充基岩 108 | chunkData.setRegion(0, 2, 0, 16, 3, 16, Material.GRASS_BLOCK); //填充草方块 109 | 110 | //将整个区块都设置为平原生物群系(PLAINS) 111 | for (int i = 0; i < 16; i++) { 112 | for (int j = 0; j < 16; j++) { 113 | b.setBiome(i, j, Biome.PLAINS); 114 | } 115 | } 116 | return chunkData; 117 | } 118 | } 119 | ``` 120 | 121 | ![](https://i.loli.net/2020/02/08/yTaJ1z7A2j4dkB6.jpg) 122 | 123 | ![](https://i.loli.net/2020/02/08/6wiDNdl8y3AmJFh.jpg) 124 | 125 | 我们进入`RuaWorld`世界, 可以发现世界按照我们所需要的地形生成了. 126 | 127 | -------------------------------------------------------------------------------- /unit/3-9.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/4-1.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/4-2.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/4-3.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdiant/BukkitDevelopmentNote/53b48ae74908150737235566e81b39c0a57ae266/unit/4-3.md -------------------------------------------------------------------------------- /unit/4-4.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/4-5.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/5-1.md: -------------------------------------------------------------------------------- 1 | # 认识 NMS 和 OBC 2 | 3 | nms,即 Java 包 `net.minecraft.server`,存放的是 Minecraft 服务端游戏逻辑代码,这篇教程将会采用 `Spigot` 作为 Bukkit API(就你们日常开发用的API) 体系的例子(CraftBukkit 也是 SpigotMC 维护,且基本所有 Bukkit 分支都是基于 Spigot 开发) 4 | 5 | Spigot 的 NMS: 6 | ![Spigot的nms](https://upload.cc/i1/2021/07/17/N2VxPT.png) 7 | 8 | BukkitAPI 真的涵盖了不少东西,但是 BukkitAPI 并不是十全十美的,有时候我们确实需要使用 NMS。 9 | ~~Forge其实也有个net.minecraft.server,但他和Spigot的没半点关系~~ 10 | ## 使用 NMS 之前 11 | 12 | ![md_5](https://secure.gravatar.com/avatar/b53fd878a84d268da2b6456e0b96cae5?s=96&d=https%3A%2F%2Fstatic.spigotmc.org%2Fstyles%2Fspigot%2Fxenforo%2Favatars%2Favatar_male_m.png)**:** *Wait!* 你真的需要使用 NMS 吗? 13 | > NMS不是API。当你遇到什么想做的事情的时候,你不应该第一时间去考虑 NMS 或者 发包 14 | 令人费解的是,我们几乎每天都看到人们专门使用 NMS 做一些简单的事情,如 ScoreBoard、BossBar 或粒子。但是实际上,自从Mojang添加了这些东西之后,Spigot/Bukkit-API 早就有这些功能了。 15 | 16 | 每当你考虑使用 NMS 时,请思考以下问题: 17 | 1. 我是否需要NMS来做这个? 18 | 2. 是否有一个API来实现这个功能? 19 | 3. 我可以为这个贡献/创建/ ***提议*** 一个API吗? 20 | 21 | 22 | 对NMS的滥用造成的后果非常严重。 23 | - 1. 插件将失去版本迁移的能力(针对单个版本而言) 24 | - 2. 阻碍了 API 的发展并且树立了一个坏榜样。 25 | 26 | 如果你确实想清楚确实没有现有的 API 能帮到你,那么来... 27 | 28 | # 怎么使用 NMS 29 | NMS 里的内容太多,故本教程**不会**教授NMS有什么东西,但是可以教你怎么玩。 30 | 31 | ## 开发环境准备 32 | *如果你使用`直接导入服务端核心到IDE`里的方法,此章节可跳过* 33 | ~~Tdiant竟然不在前面写怎么配置BukkitAPI开发环境的吗~~ 34 | 通过在 IDE 中尝试自动补全可以判断是否有 NMS: 35 | ![有NMS](https://upload.cc/i1/2021/07/17/E8sQ5k.png) 36 | 如果没有且你正在使用 `Gradle` , `Maven` 这样的依赖管理器,考虑如下方法(图文): 37 | 1. 添加源 38 | ![](https://upload.cc/i1/2021/07/17/tsDqVI.png) 39 | 2. 打开 https://repo.codemc.io/#browse/browse:nms-local 40 | ![](https://upload.cc/i1/2021/07/17/r2pPFG.png) 41 | 42 | ## 获取到一个来自 API 背后的对象 43 | *当你从控制台直接输出一个 `Player` 对象时,会发生什么?* 44 | 你会得到一个 `CraftPlayer{name=玩家名}`而不是NMS里面的`EntityPlayer`。这是因为在Bukkit-API背后还有一层,他叫`OBC`,也就是`org.bukkit.craftbukkit`。 45 | OBC 是 Bukkit API 的实现,其本质是NMS的封装,因此我们并不需要太关心它。 46 | 就上文问题,怎么拿到一个 NMS 里面的 `EntityPlayer`? 47 | 首先我们需要使用反编译工具来看 `CraftPlayer` 对 `EntityPlayer` 做了什么.. 48 | ![](https://upload.cc/i1/2021/07/17/vSEPri.png) 49 | CraftPlayer 将 EntityPlayer 传给了他的父类构造器,我们接着追踪.. 50 | ![](https://upload.cc/i1/2021/07/17/O4uWAo.png) 51 | 还是在传! 52 | ![](https://upload.cc/i1/2021/07/17/Q3UfyC.png) 53 | 经过一条不 是 很 长的继承链后,我们找到了 `CraftEntity`,看来 `EntityPlayer` 最后是被传道这里了! 54 | > 注意:OBC里面的类 `implements` 的都是 BukkitAPI 的内容,不要搞混了! 55 | 56 | 然后往上翻,看看 entity 是什么情况。 57 | ![](https://upload.cc/i1/2021/07/17/uai0Ak.png) 58 | 他被`protected`了,这意味着只有继承树内或者同一个包里面才能访问到它,而这在NMS/OBC中是常有的事情。 59 | 60 | ## 反射! 61 | 我们很容易就可以写出这样的代码来获取到这个 entity 对象。 62 | ```java 63 | public static final String serverVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; 64 | try{ 65 | Player player = ....; // Who cares ? 66 | Class clazz = Class.forName("org.bukkit.craftbukkit."+serverVersion+".entity.CraftEntity"); 67 | Field f = clazz.getDeclaredField("entity"); 68 | f.setAccessible(true); 69 | Object result = f.get(player); 70 | }catch(Throwable t){ 71 | t.printStackTrace(); 72 | } 73 | ``` 74 | 现在 `result` 里面存的就是我们需要的 EntityPlayer,然后我们可以转换它...做点事情。 75 | 为什么要这么麻烦?其实下面的代码一样可以做到这个效果: 76 | ```java 77 | try{ 78 | Player player = ....; // Who cares ? 79 | Field f = CraftEntity.class.getDeclaredField("entity"); 80 | f.setAccessible(true); 81 | Object result = f.get(player); 82 | }catch(Throwable t){ 83 | t.printStackTrace(); 84 | } 85 | ``` 86 | 甚至如果没有访问限制还可以一行搞定。 87 | 88 | ## 版本兼容性 89 | 90 | 实际上,如果你直接采用了 `CraftEntity.class` 的方法都会对这个 class 建立符号引用。 91 | 每个版本的 Spigot,无论是 OBC 或者 NMS,他们的包名都会变化——也就是说你的插件会爆炸 92 | 所以,如果你不想为了一个版本再把逻辑写一次,最好还是用反射的写法。对于公开的方法,也可以使用更高效的 `MethodHandle`。 93 | 94 | 如果你有注意到的话, md_5 说过 `NMS 并不是`一个API(其实也包括OBC)。 95 | 什么意思呢?也就是`使用NMS没有任何安全性保障`,**你反射的字段/方法或许下一个版本就会被删改。** 96 | 实例惨案: Minecraft 1.17 Spigot大改,使用 Mojang 官方混淆表,以往 NMS 插件**全都**报废。(除了一些自带兼容的服务端) 97 | 98 | # 本章小结 99 | - NMS是`net.minecraft.server`,一个包名,放MC逻辑 100 | - Spigot的NMS没有安全保障,md_5 都不推荐用 101 | - 使用NMS之前要先找是否已经有了对应的 API 102 | - 在 Bukkit-API 和 NMS 之间还有一个实现,它叫 OBC。 -------------------------------------------------------------------------------- /unit/5-2.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/5-3.md: -------------------------------------------------------------------------------- 1 | 本章暂未编写完成 2 | -------------------------------------------------------------------------------- /unit/pics/0-1-pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdiant/BukkitDevelopmentNote/53b48ae74908150737235566e81b39c0a57ae266/unit/pics/0-1-pic1.jpg -------------------------------------------------------------------------------- /unit/pics/1-1-pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdiant/BukkitDevelopmentNote/53b48ae74908150737235566e81b39c0a57ae266/unit/pics/1-1-pic1.jpg -------------------------------------------------------------------------------- /unit/pics/1-1-pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdiant/BukkitDevelopmentNote/53b48ae74908150737235566e81b39c0a57ae266/unit/pics/1-1-pic2.png -------------------------------------------------------------------------------- /unit/pics/1-4-pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdiant/BukkitDevelopmentNote/53b48ae74908150737235566e81b39c0a57ae266/unit/pics/1-4-pic1.jpg -------------------------------------------------------------------------------- /unit/pics/1-4-pic2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdiant/BukkitDevelopmentNote/53b48ae74908150737235566e81b39c0a57ae266/unit/pics/1-4-pic2.jpg --------------------------------------------------------------------------------