LV. 15
GP 17

【情報】自製bukkit插件教學 - Reactant簡易開發術

樓主 迷你烤餅 s091424
GP16 BP-
an elegant Kotlin plugin framework for spigot



前言
於有人想學習插件開發但不知道從何開始、或是覺得開發插件很麻煩,這篇教學文章針對這部分人應運而生。
本教學以Kotlin語言為主,使用Reactant插件為我們提供許多便利的工具,它實現了
依賴注入及封裝了常用功能。此外, Reactant CLI 亦提供了快捷的方式, 透過模板生成插件專案及預設class,只需幾個命令即可開始開發插件,避免了煩雜的設定,使開發過程變得更簡單。但我們不建議使用Java,因為你可能會遇到問題而且我們不會為此提供支援。

本篇文章將以Reactant 0.1.3版本及Minecraft 1.14版本為基礎。

章節目錄 (依照編寫日期順序)
0. 開發環境
1. 創建插件專案
2. 首個組件 (Component)
3. 事件監聽器 : 仙人掌刺針玩家
4. 設定檔 (第一章)
5. 指令 (Picocli指令) (第一章)
6. 指令 (Picocli指令) (第二章)
7. 設定檔 (第二章)
本教學僅提供Reactant教學,並不提供Kotlin基礎教學。



零、開發環境 (有經驗者可跳過)
在開始插件開發前,你必須準備好一個開發環境。

1. Java SE Development Kit (JDK)
進入網站之後,拉下一點,選擇相應的JDK進行下載及安裝。
如圖所示,例如電腦系統是Windows 64bit的就按紅框內的文字。


按紅圈內的格子,然後按Download,之後跟著說明安裝便可。
補充: 現在Oracle要你注冊才能下載了.... X(

2. Intellij IDEA
你需要一個IDE,不用IDEA也可以,但是我私心推薦,下面也是用IDEA做教學。

紅框內是免費版本的下載,功能也很齊全。


一、創建插件專案
首先下載Reactant CLI,解壓縮。

你會看到有這三個檔案,打開cmd然後換目錄到解壓縮的資料夾,輸入以下指令:
reactant new TutorialPlugin
其中TutorialPlugin是專案名稱,可以自己改。
之後輸入一連串的資料,就會自動幫你生成一個專案。
甚麼? 如果你不會「打開cmd然後換目錄到解壓縮的資料夾」這個操作的話,Google是你的好夥伴,自主學習是很重要的一部分。

完成了你會看到有個資料夾,那麼你就成功了。


× 選擇了AutoDeploy的話,需要自行到build.gradle.kts修改路徑。



二、首個組件 (Component)
打開idea然後開啟剛剛創建好的專案,會需要一段時間讓gradle下載檔案。

在右邊你會看到檔案架構



你會發現首次生成的代碼是這樣的

我們先不理,創建一個新的Kotlin Class,叫HelloWorld,再改成下面的樣子:
package me.clayclaw.tutorialplugin

import dev.reactant.reactant.core.component.Component
import dev.reactant.reactant.core.component.lifecycle.LifeCycleHook

@Component
class HelloWorld : LifeCycleHook {

    override fun onEnable() {
        TutorialPlugin.log.info("Hello World!!!")
    }

}

完成!
現在我們只需要把插件建置就好了,在右手邊找Gradle,選擇build

build完成之後可以在資料夾中的build\libs找到插件
甚麼? 有三個jar檔案? 我們只要第一個: TutorialPlugin-0.0.1.jar

將插件及reactant-0.1.3-all.jar放進伺服器中的plugins資料夾,運行伺服器。
看到這一行你就成功了


甚麼是Component?
Reactant會根據@ReactantPlugin所提供的package,從中搜尋組件,然後加載。
只要class上面有個@Component那就變成組件了。
甚麼時候用LifeCycleHook?
LifeCycleHook提供了onEnable(), onDisable(), onSave()三種方法供使用,也有LifeCycleInspector,但是暫不作介紹。



三、事件監聽器: 仙人掌刺針玩家
下面會教你們寫一個當玩家被怪物攻擊時就會將傷害反彈給攻擊者的功能。
如果你以前有使用過Bukkit API的話,你可能會想到要輸入registerEvents()那些的...
在Reactant我們推薦你使用內建的EventService去處理事件。

如常,我們先創建一個Component,改成下面的樣子
package me.clayclaw.tutorialplugin

import dev.reactant.reactant.core.component.Component
import dev.reactant.reactant.core.component.lifecycle.LifeCycleHook
import dev.reactant.reactant.core.dependency.injection.Inject
import dev.reactant.reactant.service.spec.server.EventService

@Component
class SpikyPlayerListener(

        @Inject
        private val eventService : EventService

) : LifeCycleHook {

    override fun onEnable() {
        
        eventService.registerBy(this) {
            
        }
        
    }

}

這樣就可以準備事件監聽了
現在我們要監聽玩家被攻擊的事件

改成這個樣子
eventService.registerBy(this) {

    EntityDamageByEntityEvent::class.observable()
            .filter { it.entity is Player } // 如果被傷害的生物是玩家的話
            .filter { it.damager is Monster } // 如果攻擊者是怪物的話
            .subscribe { (it.damager as Monster).damage(it.damage) } // 那麼就讓怪物吃同樣的傷害


}

EntityDamageByEntityEvent是事件,可以在BukkitAPI的javadoc找到詳細的列表。
Reactant其中一個特色就是使用鏈式處理事件,省下不少代碼。

如果要設置事件順序(EventPriority)為高的話, 改成:
eventService.registerBy(this) {

    EntityDamageByEntityEvent::class.observable(EventPriority.HIGH)
            .filter { it.entity is Player }
            .filter { it.damager is Monster }
            .subscribe { (it.damager as Monster).damage(it.damage) }
    

}

× 如果primary constructor內的@Inject()內沒有參數,可省略,上面只是為了讓你更好的理解
× 只有Component能夠使用@Inject

最後建置插件,進入伺服器內驗收成果。



四、設定檔 (第一章)
寫插件的時候處理設定檔有時候挺麻煩的,Reactant提供了另外一種方法讓你更輕鬆。
那麼先讓我們為上面的仙人掌玩家加個設定檔吧!
首先增加config package,新增一個class SpikyPlayerConfig:
package me.clayclaw.tutorialplugin.config

class SpikyPlayerConfig {

    var playerList : ArrayList<String> = arrayListOf()

}
回到我們的SpikyPlayerListener,改成以下的樣子:
package me.clayclaw.tutorialplugin

import dev.reactant.reactant.core.component.Component
import dev.reactant.reactant.core.component.lifecycle.LifeCycleHook
import dev.reactant.reactant.core.dependency.injection.Inject
import dev.reactant.reactant.service.spec.config.Config
import dev.reactant.reactant.service.spec.server.EventService
import me.clayclaw.tutorialplugin.config.SpikyPlayerConfig
import org.bukkit.entity.Monster
import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent

@Component
class SpikyPlayerListener(

        @Inject
        private val eventService : EventService,
        @Inject("plugins/TutorialPlugin/spikyPlayer.yml")
        private val config : Config<SpikyPlayerConfig>

) : LifeCycleHook {

    override fun onEnable() {

        eventService.registerBy(this) {

            EntityDamageByEntityEvent::class.observable()
                    .filter { it.entity is Player }
                    .filter { it.damager is Monster }
                    .filter { config.content.playerList.contains(it.entity.name) } // 看看config中有沒有該玩家的名字
                    .subscribe { (it.damager as Monster).damage(it.damage) }


        }

    }

}
這樣就成功了,Reactant會幫你自動創建檔案及讀取,而你只需要短短幾步就能完成設定檔。
現在只有config中的玩家受到攻擊才會有仙人掌效果
打開設定檔你會看到


× Reactant除了.yml外,也支援json及toml檔案



五、指令 (Picocli指令) (章節一)

Reactant使用Picocli,有興趣可參考: https://picocli.info
本章節只會介紹基礎使用方法,你可以從上述網站找到更多資訊。

有用過BukkitAPI的,使用Reactant可以讓你不需要在plugins.yml注冊指令。

首先創建一個commands package
然後新增一個Component叫CommandRegistery
package me.clayclaw.tutorialplugin.command

import dev.reactant.reactant.core.component.Component
import dev.reactant.reactant.core.component.lifecycle.LifeCycleHook
import dev.reactant.reactant.core.dependency.injection.Inject
import dev.reactant.reactant.extra.command.PicocliCommandService

@Component
class CommandRegistery(

        @Inject
        private val commandService: PicocliCommandService

): LifeCycleHook {

    override fun onEnable() {

        commandService.registerBy(this) {

            

        }

    }

}

對,相信聰明的你已經發現了: 這個格式跟EventService十分相似

接著我們來新增一個CommandHi的Class
package me.clayclaw.tutorialplugin.command

import dev.reactant.reactant.extra.command.ReactantCommand
import org.bukkit.Bukkit
import picocli.CommandLine

@CommandLine.Command(
        name = "hi", // 指令名稱,就是/hi能執行以下內容
        aliases = ["hidad", "himom"], // 選擇性,除了/hi之外都能夠執行以下內容
        mixinStandardHelpOptions = true, // 建議開啟,會自動產生--help內容
        description = ["Say HI!"] // 指令介紹
)
class CommandHi : ReactantCommand() {

    @CommandLine.Parameters(index = "0", paramLabel = "PLAYERNAME", description = ["Target Player"])
    var playerName : String = "unknown"

    override fun run() {

        if(Bukkit.getPlayer(playerName) == null) { // 找不到玩家的話
            sender.sendMessage("$playerName 沒有上線") // 送他訊息
            return // 停止執行下面內容
        }

        Bukkit.getPlayer(playerName)!!.sendMessage("${sender.name}> HI :D")

    }

}
小心別import了dev.reactant.reactant.core.commands.ReactantCommand
這個指令是/hi <玩家名稱>
輸入之後會對該玩家說HI :D


完成了之後,它不是Component所以不會被載入。
所以我們要回到CommandRegistery幫他注冊:
override fun onEnable() {

    commandService.registerBy(this) {

        command({ CommandHi() })

    }

}

可以建置並進入伺服器驗收成果。

× Command Class不是Component那怎麼用@Inject呢?
可以在primary constructor加上service,然後利用CommandRegistery協助我們取得,改成下面樣子就可以了
package me.clayclaw.tutorialplugin.command

import dev.reactant.reactant.example.HelloService
import dev.reactant.reactant.extra.command.ReactantCommand
import org.bukkit.Bukkit
import picocli.CommandLine

@CommandLine.Command(
        name = "hi", // 指令名稱,就是/hi能執行以下內容
        aliases = ["hidad", "himom"], // 選擇性,除了/hi之外都能夠執行以下內容
        mixinStandardHelpOptions = true, // 建議開啟,會自動產生--help內容
        description = ["Say HI!"] // 指令介紹
)
class CommandHi(private val helloService: HelloService) : ReactantCommand() {

    @CommandLine.Parameters(index = "0", paramLabel = "PLAYERNAME", description = ["Target Player"])
    var playerName : String = "unknown"

    override fun run() {

        if(Bukkit.getPlayer(playerName) == null) { // 找不到玩家的話
            sender.sendMessage("$playerName 沒有上線") // 送他訊息
            return // 停止執行下面內容
        }

        Bukkit.getPlayer(playerName)!!.sendMessage("${sender.name}> HI :D")

    }

}
CommandRegistery:
package me.clayclaw.tutorialplugin.command

import dev.reactant.reactant.core.component.Component
import dev.reactant.reactant.core.component.lifecycle.LifeCycleHook
import dev.reactant.reactant.core.dependency.injection.Inject
import dev.reactant.reactant.example.HelloService
import dev.reactant.reactant.extra.command.PicocliCommandService

@Component
class CommandRegistery(

        @Inject
        private val commandService: PicocliCommandService,
        @Inject
        private val helloService: HelloService

): LifeCycleHook {

    override fun onEnable() {

        commandService.registerBy(this) {

            command({ CommandHi(helloService) })

        }

    }

}


六、指令 (Picocli指令) (章節二)

這章節我們會做subcommand
讓你輸入/hi tp <玩家名稱>跟玩家say hi再傳送到他的位置

先創建SubCommandTP
Class:
package me.clayclaw.tutorialplugin.command

import dev.reactant.reactant.extra.command.ReactantCommand
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import picocli.CommandLine

@CommandLine.Command(
        name = "tp",
        mixinStandardHelpOptions = true,
        description = ["Say HI and teleport"]
)
class SubCommandTP : ReactantCommand() {

    @CommandLine.Parameters(index = "0", paramLabel = "PLAYERNAME", description = ["Target Player"])
    var playerName : String = "unknown"

    override fun run() {

        if(Bukkit.getPlayer(playerName) == null) {
            sender.sendMessage("$playerName 沒有上線")
            return
        }
        if(sender !is Player) {
            sender.sendMessage("只有玩家才能執行傳送")
            return
        }

        Bukkit.getPlayer(playerName)!!.sendMessage("${sender.name}> HI :D")
        (sender as Player).teleport(Bukkit.getPlayer(playerName)!!)

    }


}

對,就是從CommandHi那邊移來再改的
現在CommandHi改成這樣:
package me.clayclaw.tutorialplugin.command

import dev.reactant.reactant.extra.command.ReactantCommand
import picocli.CommandLine

@CommandLine.Command(
        name = "hi",
        aliases = ["hidad", "himom"],
        mixinStandardHelpOptions = true,
        description = ["Say HI!"]
)
class CommandHi : ReactantCommand() {

    override fun run() {
        showUsage() // 這樣只要不是輸入正確的SubCommand都會出--help
    }

}

最後在CommandRegistery注冊
package me.clayclaw.tutorialplugin.command

import dev.reactant.reactant.core.component.Component
import dev.reactant.reactant.core.component.lifecycle.LifeCycleHook
import dev.reactant.reactant.core.dependency.injection.Inject
import dev.reactant.reactant.extra.command.PicocliCommandService

@Component
class CommandRegistery(

        @Inject
        private val commandService: PicocliCommandService

): LifeCycleHook {

    override fun onEnable() {

        commandService.registerBy(this) {

            command({ CommandHi() }) {
                
                command( { SubCommandTP() } ) // 放在一個command的括號內能夠注冊成它的SubCommand
                
            }
            

        }

    }

}

你會發現以前的/hi <玩家名稱> 沒有用
現在改成/hi tp <玩家名稱>

你可以用以上方法增加SubCommand。
現在可以建置然後驗收成果。



七、設定檔 (第二章)
在這個章節會補充一些小技巧
相信到了這裡,你可能會問: 怎麼寫入及儲存?

那我們試試寫一個讓玩家儲存藍寶石(名字怎樣都好啦)的例子。
對 藍寶石就是一個數字而已。
class StorageSapphire {

    var players : ArrayList<SapphirePlayers> = arrayListOf()

    class SapphirePlayers {

        var name : String = "Unknown"
        var sapphire : Int = 0

    }

}

在這你會發現,其實Reactant是支援用class來寫config架構
跟第一章同樣的,先做同一樣的事但是會加點料

在Primary Constructor加上Inject
@Inject("plugins/CoreAddition/SapphireShop/playerdata.json")
private val sapphireConfig: Config<StorageSapphire>

我們想把資料給拉出來到HashMap
var sapphirePlayers : HashMap<String, Int> = hashMapOf()

override fun onEnable() {

    sapphireConfig.content.players.forEach { sapphirePlayers[it.name!!] = it.sapphire }

}

但是最關鍵的寫入儲存呢?
override fun onSave() {
    sapphireConfig.content.players.clear() // 清除config舊資料

    // 這一段是寫入
    sapphirePlayers.forEach { map ->

        sapphireConfig.content.players.add(StorageSapphire.SapphirePlayers().let {
            it.name = map.key
            it.sapphire = map.value
            it
        })
// 用with也可以

    }
    sapphireConfig.save().blockingAwait() // 儲存
}
就是這麼簡單,幾行就完成。
簡單來說就是清除config中的資料,寫入,儲存。



未完待續,歡迎GP支持。
文章內容有錯/漏的話,歡迎指出。
轉載請注明來源,若能以站內信通知本人更佳。


16
-
LV. 15
GP 17
2 樓 迷你烤餅 s091424
GP0 BP-
留位補 (上面不夠用再補)
0
-
LV. 1
GP 1
3 樓 蛋糕 zx45888190
GP0 BP-
卡個位

0
-
LV. 7
GP 40
4 樓 笑面修羅藍瑜嘯 l1123902639
GP0 BP-
卡個位,晚點回來看


0
-
未登入的勇者,要加入 5 樓的討論嗎?
板務人員:

1267 筆精華,11/11 更新
一個月內新增 7
歡迎加入共同維護。


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】