Nukkit in Kotlin #3

# Nukkit in Kotlin #3
第三回目です。#2は[こちら](https://qiita.com/UramnOIL/items/216a8698f63267f3bb78)

## やること

今回はAPIについて解説します。

### 外部APIの利用

他プラグインが提供しているAPIは多くありますが、今回は`oneborn`さんの`EconomyAPI`を使用します。
多くのプラグインは`ServiceManager`というAPIを提供するための機能を使っておりません。~~もちろんもれなく`EconomyAPI`も例外ではなく、その上PluginBaseを継承したクラスにいろいろとメソッドを追加してしまっています。利用側としてはたまったものではありませんが、広く使われているらしいので我慢しましょう。~~
`ServiceManager`については[自作APIの作り方](#自作APIの作り方)で解説します。

まず、EconomyAPIをインストールするために`build.gradle.kts`の一部を以下のように変更してください。変更する個所は適宜変えてもらっても構いません。

```build.gradle.kts
repositories {
    jcenter()
    maven {
        url = uri("https://repo.nukkitx.com/main/")
    }
    maven {
        url = uri("http://jenkins.onebone.me/plugin/repository/everything/")    //リポジトリがあるurl
    }
}

dependencies {
    implementation(kotlin("stdlib"))
    compileOnly("me.onebone", "economyapi", "2.0.1")
    compileOnly("cn.nukkit", "nukkit", "1.0-SNAPSHOT")    //グループID, アーティファクトID, バージョン
}

nukkit {
    main = "com.example.sample.SamplePlugin"
    api = listOf("1.0.9")
    authors = listOf("your name")
    depend = listOf("EconomyAPI")
}
```

`build.gradle.kts`を変更する以外は大したことは行っていません。
リポジトリを指定し、`dependencies`で対象のプロジェクトの情報を`compileOnly`の引数に指定します。

```LoginBonusListener.kt
package com.example.sample

import cn.nukkit.event.EventHandler
import cn.nukkit.event.Listener
import cn.nukkit.event.player.PlayerJoinEvent

import me.onebone.economyapi.EconomyAPI

class LoginBonusListener : Listener {
    @EventHandler
    fun onPlayerJoin(event: PlayerJoinEvent) {
        EconomyAPI.getInstance().addMoney(event.player, 10);
    }
}
```

だいたいの使い方はGitHubなどに記載されています。

### 自作APIの作成

こちらが本記事のメインです。

API作成の手順は少し特殊です。
外部にはインターフェースのみ公開し、実装は漏らさないようにしましょう。

ディレクトリの構成は下記になります。

```
Sample/
  ├plugin/
  │  ├src/main/kotlin/com/example/sample/
  │  │  ├SamplePlugin.kt
  │  │  └SampleServiceImpl.kt
  │  └build.gradle.kts
  ├interface/
  │  ├src/main/kotlin/com/example/sample/SampleService.kt    //インターフェース
  │  └build.gradle.kts
  ├build.gradle.kts
  ├setting.gradle.kts
  ├...

```

複数のプロジェクトが一つにまとまっているマルチプロジェクト構成です。プロジェクトごとに分けても構いません。
基本的にbuild.gradle.ktsが存在するディレクトリの名前がサブプロジェクト名になります。

`ServiceManager`を使って外部に公開する際は、インターフェースとそれを実装したクラスを用意します。利用側が使うのはインターフェースのみです。

```settings.gradle.kts
rootProject.name = "Sample"

include("plugin", "interface")
```

includeにサブプロジェクトの名前を一つ一つ指定していきます。

```Sample/build.gradle.kts
plugins {
    kotlin("jvm") version "1.3.61"
    id("com.github.johnrengelman.shadow") version "5.2.0"
}

allprojects {
    group = "com.example"

    repositories {
        jcenter()
        maven {
            url = uri("https://repo.nukkitx.com/main/")
        }
    }
}

tasks {
    compileKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    compileTestKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
}
```

rootディレクトリ直下にあるbuild.gradle.ktsにはプロジェクト全体の設定などを主に書きます。

プラグイン本体をビルドするためのスクリプトです。

```Sample/plugin/build.gradle.kts
plugins {
    kotlin("jvm")
    id("com.github.johnrengelman.shadow")
    id("net.minecrell.plugin-yml.nukkit") version "0.3.0"
}

version = "0.1.0"

tasks.shadowJar {
    archiveBaseName.set("Sample")    //成果物の名前 これがないとサブプロジェクトのディレクトリ名で名前が出力されてしまう
}

dependencies {
    implementation(kotlin("stdlib"))
    compileOnly(project(":interface"))    //サブプロジェクトへの依存関係。project(":<サブプロジェクト名>")で指定できる
    compileOnly("cn.nukkit", "nukkit", "1.0-SNAPSHOT")
}

nukkit {
    name = "Sample"
    main = "com.example.sample.SamplePlugin"
    api = listOf("1.0.9")
    author = "UramnOIL"
}
```

外部公開用のインターフェースをビルドするためのスクリプトです。

```Sample/interface/build.gradle.kts
plugins {
    kotlin("jvm")
    id("com.github.johnrengelman.shadow")
}

version = "0.1.0"

tasks.shadowJar {
    archiveBaseName.set("SampleInterface")    //成果物の名前 これがないとサブプロジェクトのディレクトリ名で名前が出力されてしまう
}

dependencies {
    compileOnly(kotlin("stdlib"))    //外部に公開するプロジェクトのStdlibはcompileOnlyで指定する implementationで指定するとIDEで依存関係を解決できなくなる
    compileOnly("cn.nukkit", "nukkit", "1.0-SNAPSHOT")
}
```

---

ここまでがGradle側の設定です。
Gradleを初めて使う方には地獄の作業だと思います。コピペで構いません。

```SampleService.kt
package com.example.sample

import cn.nukkit.Player

interface SampleService {
    fun sendHoge(player: Player)
}
```



外部公開用のインターフェースです。コンストラクタにリポジトリ等を渡しサービス上で操作するのが基本ですが、今回は簡単に`Player`と`String`引数に取るメソッドを一つだけ用意します。

```SampleServiceImpl.kt
package com.example.sample

import cn.nukkit.Player

class SampleServiceImpl : SampleService {
    override hoge(player: Player) {
        player.sendMessage("Hoge")
    }
}
```

インターフェースに対し実際に行う処理を書きます。

```SamplePlugin.kt
package com.uramnoil.ktform

import cn.nukkit.plugin.PluginBase
import cn.nukkit.plugin.service.ServicePriority

class KtFormPlugin : PluginBase() {
    override fun onEnable() {
        server.serviceManager.register(SampleService::class.java, SampleServiceImpl(), this, ServicePriority.Normal)
    }
}
```

サービスを登録します。
ServiceManager#register()の第一引数にはインターフェースの`Class`クラス、第二引数にはインターフェースを実装したクラスのインスタンス、第三引数にはサービスを提供しているプラグインクラスのインスタンス、第四引数にはServicePriorityを渡します。

#### ビルド

- `./gradlew :plugin:shadowJar`
- `./gradlew :interface:shadowJar`

それぞれ`plugin`サブプロジェクトと`interface`サブプロジェクトの成果物を生成します。

---

これでAPIを提供するプラグインは完成です。

### ServiceManagerを使いサービスを取得する

新しくプラグインを作ります。

####ディレクトリ構成

```
Sample2/
  ├libs/compile_only/Sample-1.0.0-all.jar
  ├...
```

`Sample2`ディレクトリに`libs/compile_only`ディレクトリを作成し、`interface`サブプロジェクトで生成した成果物を配置します。

``` kotlin:build.gradle.kts
plugins {
	kotlin("jvm") version "1.3.60"
	id("net.minecrell.plugin-yml.nukkit") version "0.3.0"
	id("com.github.johnrengelman.shadow") version "5.2.0"
}

group = "com.example"
version = "1.0-SNAPSHOT"

repositories {
    jcenter()
    maven {
        url = uri("https://repo.nukkitx.com/main/")
    }
}

dependencies {
    implementation(kotlin("stdlib"))
    compileOnly("cn.nukkit:nukkit:1.0-SNAPSHOT")
    compileOnly(fileTree("libs/compile_only"))
}

tasks {
    compileKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    compileTestKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    
}

nukkit {
    name = "Sample2"
    main = "com.example.sample2.Sample2Plugin"
    api = listOf("1.0.9")
    authors = listOf("your name")
    depend = listOf("Sample")
}
```

\#1で記述した`build.gradle.kts`を新しいプラグイン用に調整したものです。
`compileOnly(fileTree("libs/compile_only"))`で依存関係に含めるディレクトリを指定します。

``` kotlin:GreetListener.kt
package com.example.sample2

import cn.nukkit.event.EventHandler
import cn.nukkit.event.Listener
import cn.nukkit.event.player.PlayerJoinEvent
import cn.nukkit.plugin.service.RegisteredServiceProvider

import com.example.sample.SampleService

class HogeListener() : Listener {
    val sampleService = Server.getInstance().serviceManager.getProvider(SampleService::class.java)

    @EventHandler
    fun onPlayerJoin(event: PlayerJoinEvent) {
        sampleService.provider.sendHoge(event.player)
    }
}
```

ServiceManager#getProvider()の第一引数にインターフェースのClassクラスを渡すとそのインターフェースに対応したインスタンスを保持したRegisteredServiceProviderが返ってきます。`RegisteredServiceProvider#getProvider()`でサービスクラスを取得し操作を行ってください。

``` Kotlin:Sample2Plugin.kts
package com.example.sample2

import import cn.nukkit.plugin.PluginBase

class Sample2Plugin() : PluginBase() {
    override fun onEnable() {
        server.pluginManager.registerEvents(HogeListener(), this)
    }
}
```

`./gradlew shadowJar`を実行すると成果物が生成されます。

## まとめ

結構ボリュームはありますが、この手順を踏むことで利用者へ簡単に機能を提供することができます。


## 宣伝

[これ](https://github.com/UramnOIL/KNukkitUtils)を使えばNukkitプラグインをもっと楽に、Kotlinらしく記述することができます。