本帖最后由 RarityEG 于 2021-11-3 11:50 编辑
如果你听说过服务端插件,你的第一反应一定是:“好复杂啊!”特别是像 EssentialsX 这样的大型插件,功能多到一个配置文件都快装不下了。你可能会觉得:“反正我就用别人的插件就可以了,自己写…”于是就放弃了插件开发的梦想。
其实,你觉得插件开发遥不可及,并不是因为插件开发高深莫测,也不是因为插件开发技术超前,而是因为没有人告诉你那其实是一件很简单的事情。
那么,如果再给你一次机会,让你跟着我一起学习插件开发,你愿意吗?
本教程就是基于此创建的。
教程入口:
https://plgdev.xuogroup.top

遇到问题?
那么大个回复按钮是看不见吗(不,要有耐心)请回复说明!
最近一次更新的说明:Done!Yeah!
完结撒花!
完结于:2021/2/28 13:26
(深夜还在疯狂码字……心疼心疼我的蹄蹄吧……嘤~)
这是
一份理论与实践结合的插件开发教程
从基础之基础,到终极之终极
阅读本教程几乎不需要基础!只要你能够熟练地操作电脑就可以了。
付费插件功能很强买不起?那有什么!我们自己来写一个!

Cloudflare Pages 实时更新:
无法访问?也可以使用原始域名:https://plugin-diary.pages.dev
注意!
- 站点使用 Docsify 呈现,首次访问较慢,第二次访问速度就会变快!
- 教程内容非常紧凑,请仔细阅读!
GitHub Pages 在部分地区无法访问- EDIT: 本教程已改用 Cloudflare Pages,旧链接仍然有效哦~
- (打不开请把你 DNS 改为 1.1.1.1 (iPv4) 或 2606:4700:4700::1111(iPv6))
- (谁再说访问不了,我…我也不能把你怎么样
)
遇到问题?
EDIT 2021/08/06: 有关本教程的 Discord 服务器已建立,欢迎来玩(可私信我获得邀请链接哦!)
1,你没有对玩家聊天、受伤等事件作取消,这意味着如果op使用你的插件登录,其帐号将有风险。
2,
复制代码
应该改为
复制代码
2,
- public static boolean verifyPassword(String playerName, String password) {
- if (!isPlayerRegistered(playerName)) {
- return false;
- }
- FileConfiguration config = HarmonyAuth.instance.getConfig();
- return password.equals(config.getString(playerName.toLowerCase()));
- }
应该改为
- public static boolean verifyPassword(String playerName, String password) {
- return password.equals(config.getString(playerName.toLowerCase()));
- }
勘误:3-1中
请看CraftServer的代码
复制代码
创造模式那个包括了全部物品的物品栏打开这件事是客户端行为,服务端管不着,因此不算
那么全游戏最大的物品栏是大箱子的54格,54=6*9
所以应当是 1*9 到 6*9
Bukkit.createInventory 用于创建一个 Inventory,默认的物品栏就是箱子 GUI,所以无需修改。
括号内的三个参数分别是所属实体(这里通过服务端获取了一个名为「RarityEG」的玩家),大小(必须是 9 的倍数,从 1 * 9 到 9 * 9),标题。
请看CraftServer的代码
- public Inventory createInventory(InventoryHolder owner, int size, String title) throws IllegalArgumentException {
- Validate.isTrue((9 <= size && size <= 54 && size % 9 == 0), "Size for custom inventory must be a multiple of 9 between 9 and 54 slots (got " + size + ")");
- return CraftInventoryCreator.INSTANCE.createInventory(owner, size, title);
- }
创造模式那个包括了全部物品的物品栏打开这件事是客户端行为,服务端管不着,因此不算
那么全游戏最大的物品栏是大箱子的54格,54=6*9
所以应当是 1*9 到 6*9
你可以在 IDEA 中通过反编译查看这些类
在服务端,UUID 的使用并不是很频繁,大多数情况下,使用玩家名就足以区分了。基本上能用 UUID 的地方也都能用名字代替。
Bukkit.getPlayer("Hacker").banPlayerFull("使用外挂被 Ban!");
我完全没找到这个方法。
顺便一提,上次我提出的两个问题你也没改啊
洞穴夜莺 发表于 2021-2-5 22:00
何必呢,hub.spigotmc.org上有代码原文啊。
不都在推荐uuid代替掉名字吗,而且uuid判断相等比字符串快
那个ban应该是要通过服务器的banlist
而且我记得好像文档说是ban要改动,但是事实上至今又没改动
洞穴夜莺 发表于 2021-2-5 22:00
何必呢,hub.spigotmc.org上有代码原文啊。
banPlayerFull 位于 https://papermc.io/javadocs/pape ... l-java.lang.String-
可能是因为我用的 Paper 吧,我会修正这部分
---
对上次内容的说明:
该部分内容是演示,并非实际应用,阅读到那一章时,读者可能才刚开始认识 EventHandler,这时候就不要把代码再搞复杂了(重点不一样)。
后面的一个项目中会重新关注这类问题
RarityEG 发表于 2021-2-5 22:14
banPlayerFull 位于 https://papermc.io/javadocs/paper/1.16/org/bukkit/entity/Player.html#banPlayerF ...
- 那么上次的第二个提议呢?
- 请始终使用UUID辨识玩家
RarityEG 发表于 2021-2-5 22:30
UUID的获取方法是啥来着查JavaDocs 查了半天没找到
问题2已修复
OfflinePlayer#getUniqueId()
水群的时候突然发现,慕名而来
哈哈哈哈人 间 真 实
打开以后直接点了NMS与反射那块,发现没写,挺可惜,GKD
读起来的话总体感觉是没什么废话的,整体上无论是思路还是内容都是比较简单清晰的,
我个人比较喜欢这种用实际事例举例的说明方法,
个人感觉帖子里的“行动规划”式的写法挺有意思的,如果要是能写的促进读者边看边做的话,那内容会很成功
其实我一直觉得设置Lib应该算Java基础(划掉
不过确实有一些错误的地方,(整理完以后发现已经有dalao指出来了),好在整体内容是没什么错误的,
有些地方个人觉得详略处理有些问题,比如插件的调试,从配置服务端开始讲,个人认为准备开发插件的话,应该具有开启一个服务端的能力才对
新教程肯定有不足之处,多多改正就好,总之就是非常不错,加油
哈哈哈哈
package ... 语句根据你的包名而决定,直接照搬的后果我不负责!
打开以后直接点了NMS与反射那块,发现没写,挺可惜,GKD
读起来的话总体感觉是没什么废话的,整体上无论是思路还是内容都是比较简单清晰的,
我个人比较喜欢这种用实际事例举例的说明方法,
个人感觉帖子里的“行动规划”式的写法挺有意思的,如果要是能写的促进读者边看边做的话,那内容会很成功
其实我一直觉得设置Lib应该算Java基础(划掉
不过确实有一些错误的地方,(整理完以后发现已经有dalao指出来了),好在整体内容是没什么错误的,
有些地方个人觉得详略处理有些问题,比如插件的调试,从配置服务端开始讲,个人认为准备开发插件的话,应该具有开启一个服务端的能力才对
新教程肯定有不足之处,多多改正就好,总之就是非常不错,加油
Bukkit.getPlayer("RarityEG").setResourcePack("https\://example.com/xxx.zip");
应为
Bukkit.getPlayer("RarityEG").setResourcePack("https\\://example.com/xxx.zip");
截至 2/9 上午,累积的更新内容:
- 从 3-4 一口气写到了 4-3,该部分 Typora (保守)统计超过 6000 字。(含代码)
- 删除了原定的 4-3(Apache Commons),将后面内容整体向前推一节
- 修正了 3-3 中有关 UUID 的问题
- 对第一个登录插件存在的问题进行了说明
- 改正了几处错别字
- 全站更换图床
- 移除了章目录左侧的箭头标识
- 修复了章节名过长时后半部分被截掉的问题
我来踩你啦我正好在学习插件1可以教教我么
正好打算重新开始学mc的插件开发
上次来mcbbs开发板块的时候翻到的教程都是远古时期的玩意了
感谢楼主的教程~
上次来mcbbs开发板块的时候翻到的教程都是远古时期的玩意了
感谢楼主的教程~
这里有两个事情我没看懂,我觉得在教程里还需要解释一下
1,为啥要监听受伤事件然后取消而不是直接给玩家设置无敌?设置无敌免除伤害的同时能够使玩家失去对怪物的吸引能力,不是更好?
2,为啥IDataManager每次都要重新new?
1,为啥要监听受伤事件然后取消而不是直接给玩家设置无敌?设置无敌免除伤害的同时能够使玩家失去对怪物的吸引能力,不是更好?
2,为啥IDataManager每次都要重新new?
本帖最后由 RarityEG 于 2021-2-10 21:23 编辑
第一个是我没想到的……
第二个主要是用静态类的话执行每次操作前都要做一个 if 判断选择使用哪一个 Manager,多写代码,另外由于类变量和方法都不会复制,不怎么占用内存(稍微加重了GC的负担,不过应该都能 YoungGC 掉),主要是为了展示接口的用法。
另:设置无敌用哪个方法?改游戏模式?
CaveNightingale 发表于 2021-2-10 21:07
这里有两个事情我没看懂,我觉得在教程里还需要解释一下
1,为啥要监听受伤事件然后取消而不是直接给玩家设 ...
第一个是我没想到的……
第二个主要是用静态类的话执行每次操作前都要做一个 if 判断选择使用哪一个 Manager,多写代码,另外由于类变量和方法都不会复制,不怎么占用内存(稍微加重了GC的负担,不过应该都能 YoungGC 掉),主要是为了展示接口的用法。
另:设置无敌用哪个方法?改游戏模式?
本帖最后由 CaveNightingale 于 2021-2-10 22:29 编辑
说实话我是在Mod开发里学会设置无敌的,根据注释好像是这个,但是看实现又不太像
你可以试试,用NMS肯定能搞
用NMS搞的话就简单粗暴,你可以自己找一下有没有Bukkit封装
EntityPlayer#abilities.isInvulnerable = true;
EntityPlayer#updateAbilities();
如果实在没有那就算了吧
第二个问题是
为啥不
public static final IDataManager IDM;
static {
if(......)
IDM = new IDataManagerImplementA();
else if(.......)
IDM = new IDataManagerImplementB();
else
IDM = new IDataManagerImplementC();
}
然后后面直接用IDM.xxxx
RarityEG 发表于 2021-2-10 21:21
第一个是我没想到的……
第二个主要是用静态类的话执行每次操作前都要做一个 if 判断选择使用哪一个 Manag ...
说实话我是在Mod开发里学会设置无敌的,根据注释好像是这个,但是看实现又不太像
你可以试试,用NMS肯定能搞
用NMS搞的话就简单粗暴,你可以自己找一下有没有Bukkit封装
EntityPlayer#abilities.isInvulnerable = true;
EntityPlayer#updateAbilities();
如果实在没有那就算了吧
第二个问题是
为啥不
public static final IDataManager IDM;
static {
if(......)
IDM = new IDataManagerImplementA();
else if(.......)
IDM = new IDataManagerImplementB();
else
IDM = new IDataManagerImplementC();
}
然后后面直接用IDM.xxxx
CaveNightingale 发表于 2021-2-10 22:21
说实话我是在Mod开发里学会设置无敌的,根据注释好像是这个,但是看实现又不太像
你可以试试,用NMS肯定能 ...
是 Bukkit 多线程的问题,如果全局只用一个对象可能访问冲突掉
不然就得 synchronized 同步,而和数据库的交互又比较慢,同步执行下一个线程迟迟无法访问
所以就new 一个出来
虽然也没有从根本上解决问题(static 的 username, password),但这些类变量的同步锁时间比一整个DataManager的同步锁时间短很多,而且除了loadAll是写其它都是读,基本不存在访问冲突
CaveNightingale 发表于 2021-2-10 21:07
这里有两个事情我没看懂,我觉得在教程里还需要解释一下
1,为啥要监听受伤事件然后取消而不是直接给玩家设 ...
在Bukkit中对玩家设置无敌是有现成的API的,LivingEntity的setNoDamageTicks与setMaximumNoDamageTicks,这个还是根据个人喜好来的,我感觉用的更多的还是直接监听然后取消掉
IDataManager的问题我也想吐槽 2333
前排观摩大佬
感谢楼主的分享,对我很有帮助
ohhhhhh表示感谢!!!
建议5-6中加入bungee api与原始Json文本之间的对应关系
这样看这更清晰,没那么累
https://wiki.biligame.com/mc/%E5 ... C%E6%A0%BC%E5%BC%8F
这样看这更清晰,没那么累
https://wiki.biligame.com/mc/%E5 ... C%E6%A0%BC%E5%BC%8F
哇
?????
这不是大佬吗
膜拜膜拜!
?????
这不是大佬吗
膜拜膜拜!
这么详细的教程,竟然没有优秀 =.=,像 NMS 之类的讲解可是很少有的
本帖最后由 洞穴夜莺 于 2021-2-26 20:10 编辑
提个小建议
糟糕的copy to clipboard遮挡
接下来是个大问题
https://plgdev.xuogroup.top/#/7-4
方法上的synchronized锁住的是整个对象
这一点可以通过jshell运行如下内容来验证
复制代码
提个小建议
糟糕的copy to clipboard遮挡
接下来是个大问题
https://plgdev.xuogroup.top/#/7-4
方法上的synchronized锁住的是整个对象
这一点可以通过jshell运行如下内容来验证
- class X {
- synchronized void block() {
- try{
- Thread.sleep(10000);
- }catch(Throwable ex){}
- }
- synchronized int test() {
- return 1;
- }
- }
- var k = new X();// 创建X对象
- k.test();// 返回1
- new Thread(() -> k.block()).start();// 在另外的线程调用block方法
- k.test();// 明显观察到这里会卡住
感谢楼主分享!
这个是真的不戳 nb
哇哇哇哇
超级棒!!!
6666666666
666666666666666
6666666666666
6666666666666666
66666666666666
大佬你好,这是我看你的教程写登陆插件的事件监听类,我在运行插件的时候玩家不登陆还是可以打开物品栏,但是我明明已经按照你的教程监听并且取消这个事件了,请你看看我哪里出错了
复制代码
- public class EventListener implements Listener {
- @EventHandler
- public void onPlayerLogin(PlayerLoginEvent event) {
- LoginData.addPlayerName(event.getPlayer().getName());
- }
- @EventHandler
- public void onPlayerQuit(PlayerQuitEvent event){
- LoginData.removePlayerName(event.getPlayer().getName());
- }
- @EventHandler
- public void cancelMove(PlayerMoveEvent event){
- cancelLoggedIn(event);
- }
- @EventHandler
- public void cancelInteract(PlayerInteractEvent event){
- cancelLoggedIn(event);
- }
- @EventHandler
- public void cancelInteractAtEntity(PlayerInteractAtEntityEvent event){
- cancelLoggedIn(event);
- }
- @EventHandler
- public void cancelPortal(PlayerPortalEvent event){
- cancelLoggedIn(event);
- }
- @EventHandler
- public void cancelTeleport(PlayerTeleportEvent event){
- cancelLoggedIn(event);
- }
- @EventHandler
- public void cancelOpenInventory(InventoryOpenEvent event){
- cancelLoggedIn(event);
- }
- @EventHandler
- public void noHurt(EntityDamageEvent event){
- if ((event).getEntity() instanceof Player){
- if (LoginData.hasPlayerName((event).getEntity().getName())){
- event.setCancelled(true);
- }
- }
- }
- public static void cancelLoggedIn(Cancellable event){
- if (event instanceof PlayerEvent) {
- if (LoginData.hasPlayerName(((PlayerEvent) event).getPlayer().getName())) {
- event.setCancelled(true);
- }
- } else if (event instanceof InventoryOpenEvent){
- if (LoginData.hasPlayerName(((InventoryOpenEvent) event).getPlayer().getName())){
- event.setCancelled(true);
- }
- }
- }
- }
本帖最后由 RarityEG 于 2021-3-12 20:40 编辑
主类中是否进行了注册?
复制代码
EDIT:请参见楼上回答(我没有实地测试)
wyd1542536763 发表于 2021-3-12 15:31
大佬你好,这是我看你的教程写登陆插件的事件监听类,我在运行插件的时候玩家不登陆还是可以打开物品栏,但 ...
主类中是否进行了注册?
- public void onEnable() {
- Bukkit.getPluginManager().registerEvents(new EventListener(), this);
- }
EDIT:请参见楼上回答(我没有实地测试)
wyd1542536763 发表于 2021-3-12 15:31
大佬你好,这是我看你的教程写登陆插件的事件监听类,我在运行插件的时候玩家不登陆还是可以打开物品栏,但 ...
打开物品栏是纯客户端行为,bukkit无法监听
针灸零基础 支持一波
感谢楼主分享!
真的用心了 大制作!!
感觉很强的样子。
简直大爱 太nb了
最开始的那个选JAR文件不应该是选开服之后服务器根目录里的cache文件夹里的patched吗?我就因为这个弄了好久直到看见你的示范图像为止
本帖最后由 RarityEG 于 2021-3-14 21:10 编辑
因为你用的是 Paper
GetBukkit 得到的 Spigot 就是 spigot-1.16.5.jar
wood-man 发表于 2021-3-14 17:37
最开始的那个选JAR文件不应该是选开服之后服务器根目录里的cache文件夹里的patched吗?我就因为这个弄了好久 ...
因为你用的是 Paper
GetBukkit 得到的 Spigot 就是 spigot-1.16.5.jar
RarityEG 发表于 2021-3-14 21:05
因为你用的是 Paper
GetBukkit 得到的 Spigot 就是 spigot-1.16.5.jar
建议加一句,不然真的是麻烦
牛牛牛牛牛牛牛牛牛牛
感谢大佬的教程,开发插件真的很有用!!!
牛牛牛牛牛牛牛牛牛牛