MinecraftのForge ModをScalaで書く

2021/04/07 時点での Minecraft 1.16.5 のMod (FML)をScalaで開発する方法をメモ程度に残します。簡単なメッセージを出す部分までを作ります。ほとんどMinecraft Wiki (Javaでの方法)に従っています。
書き忘れ・訂正などありましたらよろしくお願いします。


### 環境
Minecraft 1.16.5
Forge 1.16.5-36.1.0
JDK 1.8.0_281
Scala 2.13.4

sbtは使わずgradleでビルドします。



# セットアップ

Minecraft Wikiのページに従ってIntelliJ IDEAで環境を構築します。追記しつつまとめますが必要なければ読み飛ばしてください。

https://minecraft.fandom.com/wiki/Tutorials/Creating_Forge_mods

IntelliJ IDEAのインストールが前提です。

### 1. MDKの準備
ForgeのMDK(Mod Development Kit)を[Forgeの配布サイト](http://files.minecraftforge.net/) のShow all Versionsからダウンロードし、zipの中身の

- gradle/
- src/
- build.gradle
- gradlew
- gradlew.bat

をプロジェクトフォルダに配置します。

build.gradleを次のように変更します。難読化を解除する際の名前のマップだと思います(詳しく調べてません)。

```gradle:old
mappings channel: 'official', version: '1.16.5'
```

```gradle:new
mappings channel: 'snapshot', version: '20210309-1.16.5'
```



### 2. IntelliJ IDEAにインポート

IntelliJ IDEAを起動し、Open File or Project から build.gradle を選択するとインポートできます。初回は時間がかかります。インポートが完了すると画面右端にGradleのタブができるので、 `Tasks/fg_runs/genIntellijRuns` を実行します。

1でのマップの変更に伴い `src/main/ExampleMod.java` を次のように変更します。

```java:old
LOGGER.info("Got game settings {}", event.getMinecraftSupplier().get().options);
```

```java:new
LOGGER.info("Got game settings {}", event.getMinecraftSupplier().get().gameSettings);
```

### 3. 名前の変更

ExampleModとなっているため名前を変更します。2021/04/07 時点では Minecraft Wiki のページは記述途中であったため書きます。

##### Mod名, ModIDの変更

`mods.toml`を編集します。`[[mods]]`内の`modId`を変更します。その他modに関する情報は[[mods]]にあるので適宜変更・追加してください。以下に例を示します。

<details><summary>折りたたみ</summary><div>

```toml:mods.toml
modLoader="javafml"
loaderVersion="[36,)"
license="MIT"

[[mods]]
    modId="MOD_NAME"
    namespace="MOD_NAME"
    version="0.1"
    displayName="MOD_NAME"
    authors="Tsukina_7mochi"
    description='''
Created

with

Scala!
    '''

[[dependencies.MOD_NAME]]
    modId="forge"
    mandatory=true
    versionRange="[36,)"
    ordering="NONE"
    side="BOTH"

[[dependencies.MOD_NAME]]
    modId="minecraft"
    mandatory=true
    versionRange="[1.16.5,1.17)"
    ordering="NONE"
    side="BOTH"
```

</div></details>

次に `ExampleMod.java` を編集します。

`@Mod`アノテーションの引数の部分を `mods.toml` と同じにします。またクラス名を変更します。おすすめは次のような感じです(IronChestsのソースを見てきた)。

```java:ModName.java
@Mod(Chalcedony.MODID)
class Chalcedony{
    public static final String MODID =  "MOD ID";

    // ... 処理

}
```


##### パッケージ名の変更

ご自由にどうぞ
Scalaではパッケージ名とディレクトリ構成は一致していなくても問題ないですがJava同様一致させるのが慣例らしいです。



# Scalaを使えるようにする

そのままだとScalaでコーディングができないため設定をします。全体をScalaで書くとFMLから参照できない[^1]ため、FMLから呼ばれる部分のみJavaで記述します[^2]。


### IntelliJ IDEAにScalaプラグインを追加

File > Settings > Plugins からScalaプラグインを検索して導入します。導入後エディタの再起動が必要です。

https://pleiades.io/help/idea/managing-plugins.html


### build.gradleの編集

ここが2番目に躓きました(1番は全部Scalaで書けない理由を探すところ)

##### Scala Pluginの追加

ルートに次の記述を追加します。最初の方に `net.minecraftforge.gradle` とか `maven-publish` をapplyしてるところがあるのでそこに追記すればいいと思います。

```gradle:build.gralde
apply plugin: 'scala'
```

http://gradle.monochromeroad.com/docs/userguide/scala_plugin.html

##### JavaファイルとScalaファイルのジョイント

ここが肝です。114行目ぐらいの

```gradle:build.gralde
sourceSets.main.resources { srcDir 'src/generated/resources' }
```

を

```gradle:build.gralde
sourceSets {
    main {
        scala {
            srcDirs = ['src/main/scala', 'src/main/java']
        }
        java {
            srcDirs = []
        }
        resources {
            srcDir 'src/generated/resources'
        }
    }
}

```

のように変更します[^3]。


# サンプル

scalaでログインしたユーザーにメッセージを表示するロジックを書き、メインのクラスで登録します。

```scala:client.scala
package ex.client

// import省略

class LoginMessage(){
  @SubscribeEvent
  def onPlayerLoggedIn(event: PlayerEvent.PlayerLoggedInEvent): Unit = {
    val player = event.getPlayer
    val pos = player.getPosition
    val message = s"""
                     |Hello, World
                     |name = [${player.getName.getString}]
                     |pos  = [${pos.getX}. ${pos.getY}, pos.getZ]
    """.stripMargin
    player.sendMessage(new StringTextComponent(message), player.getUniqueID)
  }
}
```

```java:testmod.java
package ex

// import省略
import ex.client.LoginMessage;

@Mod(TestMod.MODID)
class TestMod{
    public static final String MODID =  "testmod";
    private static final Logger LOGGER = LogManager.getLogger();

    public Chalcedony() {
        final IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus();
        bus.addListener(this::setup);
        bus.addListener(this::enqueueIMC);
        bus.addListener(this::processIMC);
        bus.addListener(this::doClientStuff);

        MinecraftForge.EVENT_BUS.register(this);
        MinecraftForge.EVENT_BUS.register(new LoginMessage());
    }

    // ... 処理
}
```

参考にした記事: https://qiita.com/niwasawa/items/ca565091468abce48f53





[^1]: ScalaからJVMの中間言語にコンパイルすると名前に`$`サフィクスがつくためMod本体であるシングルトンオブジェクト(のコンパニオンクラス)のコンストラクタを参照できず`IllegalAccessException`が投げられるようです。<br>参照: https://stackoverflow.com/questions/17401565/instantiating-a-scala-class-using-reflection-javas-newinstance <br>参照: https://github.com/scala/bug/issues/3276
[^2]: 追加でScalaに対応するMDKやScorgeなどがあるようですが導入しなくても開発できそうです。
[^3]: `src/main/java` 以下のファイルをJavaではなくScalaとして読み込むようにしています(Scalaは単純にJavaの拡張であるためJavaのコードははScalaとして解釈可能です)。これによりパッケージの依存関係を解決しています。