☕️

Forge1.18.2のmodをScalaで開発する

2022/11/11に公開

1.16.5でScalaを使用して開発していたmodを1.18.2にアップデートしたら動かなくなったのを動くようにした話です。
Scalable Cat's Forceを使用します。

対象読者

  • Scalaが書ける/書きたい
  • forgeでmodの開発がしたい

環境

Windows 11
Minecraft 1.18.2
Forge 1.18.2-40.1.0
Java 17.0.1
Scala 2.13.8
gradle 7.4
Intellij IDEA Ultimate 2022.2.3

セットアップ

エディターはIntellij IDEA(以下IDEA)を使用します。言語は英語として進めるので日本語にローカライズした場合は適宜読み替えてください。
結果だけ見たい人用

javaとの共通部

  1. Forge公式サイトからMdkをクリックしてダウンロードしたのち、zipファイルを展開します。
  2. IDEAを起動し、Openから1で展開したフォルダーを開きます。
  3. Java17をインストールしていないまたは別のバージョンのJavaをインストールしている場合、File->Project StructureからSDK17に変更します。
  4. IDEAのgradleタブからTasks/forgegradle runs/genIntellijRunsをダブルクリックします。(gradle importが終わるまで出てこないので気長に待ちましょう)

ファイル変更

build.gradle

まず以下を追加します。

apply plugin: 'scala'

次にrepositoriesの中に以下を追加します。

maven {
    name = "Azure-SLP"
    url = uri("https://pkgs.dev.azure.com/Kotori316/minecraft/_packaging/mods/maven/v1")
    content {
        it.includeModule("com.kotori316", "ScalableCatsForce".toLowerCase())
        it.includeModule("org.typelevel", "cats-core_${scala_major}")
        it.includeModule("org.typelevel", "cats-kernel_${scala_major}")
    }
}

最後にdependenciesの中に以下を追加します。

implementation(group: 'org.scala-lang', name: 'scala-library', version: scala_version)
implementation(group: 'org.typelevel', name: "cats-core_${scala_major}", version: '2.8.5-kotori')

runtimeOnly(group: "com.kotori316", name: "ScalableCatsForce".toLowerCase(), version: "2.13.8-build-4", classifier: "with-library") {
    transitive(false)
}

gradle.properties

以下を追記します。

scala_version=2.13.10
scala_major=2.13

src/main/resource/META-INF/mods.toml

modLoader, loaderVersionをそれぞれ以下のように書き換えます。

modLoader="kotori_scala"
loaderVersion="[2.13.3,2.14.0)"

ソースコード

  1. src/main/javasrc/main/scalaに名前変更します。
  2. ソースコードはすべてsrc/main/scala/com/example/examplemod/以下に追加します。
  3. ExampleMod.javaを削除します。
  4. examplemodフォルダーを右クリック→NewScala ClassからExampleModという名前でファイルを作成します。
  5. ExampleMod.scalaの内容を以下に書き換えます。
package com.example.examplemod

import com.mojang.logging.LogUtils
import net.minecraft.world.level.block.{Block, Blocks}
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.RegistryEvent
import net.minecraftforge.event.server.ServerStartingEvent
import net.minecraftforge.eventbus.api.SubscribeEvent
import net.minecraftforge.fml.InterModComms
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.event.lifecycle.{FMLCommonSetupEvent, InterModEnqueueEvent, InterModProcessEvent}
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext


// The value here should match an entry in the META-INF/mods.toml file
@Mod("examplemod")
object ExampleMod { // Directly reference a slf4j logger
  private val LOGGER = LogUtils.getLogger

  // You can use EventBusSubscriber to automatically subscribe events on the contained class (this is subscribing to the MOD
  // Event bus for receiving Registry Events)
  @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
  object RegistryEvents {
    @SubscribeEvent
    def onBlocksRegistry(blockRegistryEvent: RegistryEvent.Register[Block]): Unit = { // Register a new block here
      LOGGER.info("HELLO from Register Block")
    }
  }
}

@Mod("examplemod")
class ExampleMod() { // Register the setup method for modloading
  FMLJavaModLoadingContext.get.getModEventBus.addListener(this.setup)
  // Register the enqueueIMC method for modloading
  FMLJavaModLoadingContext.get.getModEventBus.addListener(this.enqueueIMC)
  // Register the processIMC method for modloading
  FMLJavaModLoadingContext.get.getModEventBus.addListener(this.processIMC)
  // Register ourselves for server and other game events we are interested in
  MinecraftForge.EVENT_BUS.register(this)

  private def setup(event: FMLCommonSetupEvent): Unit = { // some preinit code
    ExampleMod.LOGGER.info("HELLO FROM PREINIT")
    ExampleMod.LOGGER.info("DIRT BLOCK >> {}", Blocks.DIRT.getRegistryName)
  }

  private def enqueueIMC(event: InterModEnqueueEvent): Unit = { // Some example code to dispatch IMC to another mod
    InterModComms.sendTo("examplemod", "helloworld", () => {
      def foo() = {
        ExampleMod.LOGGER.info("Hello world from the MDK")
        "Hello world"
      }

      foo()
    })
  }

  private def processIMC(event: InterModProcessEvent): Unit = { // Some example code to receive and process InterModComms from other mods
    ExampleMod.LOGGER.info("Got IMC {}", event.getIMCStream.map((m: InterModComms.IMCMessage) => m.messageSupplier.get).toList)
  }

  // You can use SubscribeEvent and let the Event Bus discover methods to call
  @SubscribeEvent
  def onServerStarting(event: ServerStartingEvent): Unit = { // Do something when the server starts
    ExampleMod.LOGGER.info("HELLO from server starting")
  }
}

検証

IDEAのgradleタブからTasks/forgegradle runs/runClientをダブルクリックするか、ターミナルからgradlew runClientとするとMinecraftが起動します。起動後ModsExample Modがあれば成功です。
成功した画像
jarファイルはbuild/libsに生成されます。

注意点

  • ユーザーは前提modにScalable Cat's Forceが必要となります。
  • javaのコードでMod.EventBusSubscriberを使用すると例外が発生します。
  • Scala3はサポートされていません。

参考

MinecraftのForge ModをScalaで書く
SLP

GitHubで編集を提案

Discussion