Créer la base de son mod en Kotlin



  • Sommaire

    Introduction

    Après avoir vu la configuration de l'espace de travail pour inclure du code en Kotlin dans un mod, ce tutoriel passe à l'étape suivante et explique la manière de créer un Mod 100% Kotlin. Pour cela, nous allons devoir ajouter une annotation @Mod sur la classe principale.

    Pré-requis

    L'adaptateur de langage

    Le mécanisme de de découverte et d’instanciation de Mod par FML se base sur le principe d'annotation de Java. Ainsi, voici le code minimale donné par le tutoriel de base de ce forum, en Java.

    package fr.minecraftforgefrance.tutorial;
    
    import net.minecraftforge.fml.common.Mod;
    
    @Mod(modid = ModTutorial.MODID)
    public class ModTutorial
    {
        public static final String MODID = "tutorial";
    }
    

    Lors de son démarrage, FML demande à la machine virtuelle Java de lui donner une liste de toutes les classes annotées par @Mod. Pour chacune de ces classes, une instance unique est créée. Depuis la version 1.8, une classe dérivée de ILanguageAdapter est chargée de cette recherche et instanciation. Elle est disponible de base pour Java, bien entendu, mais aussi pour Scala.

    Si vous êtes curieux, le code se trouve ici dans ce fichier : net/minecraftforge/fml/common/ILanguageAdapter.java

    Afin de pouvoir faire découvrir notre Mod en Kotlin, il y a plusieurs choix. Je vais en exposer deux principaux, et une option.

    Choix 1: se faire passer pour du Java

    Kotlin s'insère dans le monde Java et produit du code compatible lorsque les concepts sont les mêmes. Ainsi, une classe compilée depuis du Kotlin a la même forme (dans le .class), qu'une classe compilée en Java. Une première méthode (qui fonctionne peut-être avec le version antérieures) est de créer une classe Kotlin annotée.

    Voici l'équivalent du code précédent, en Kotlin.

    package fr.minecraftforgefrance.tutorial
    
    import net.minecraftforge.fml.common.Mod
    
    @Mod(modid = ModTutorial.MODID)
    public class ModTutorial
    {
        companion object {
           internal const val MODID = "tutorial"
        }
    }
    

    J'en vois qui s'agitent et se demandent bien l'intérêt de programmer en Kotlin, si déjà de base, il faut écrire plus de choses qu'avec du Java. Je les comprends. C'est ce qu'il se passe dans à peu près n'importe quel langage lorsque l'on essaie d'appliquer les concept d'un autre langage.

    Si l'on reprend le code du tutoriel précédent en Kotlin, cela donne ça :

    package org.puupuu.kotlinbasedmod
    
    import net.minecraftforge.fml.common.Mod
    import net.minecraftforge.fml.common.Mod.EventHandler
    import net.minecraftforge.fml.common.event.FMLInitializationEvent
    import net.minecraftforge.fml.common.event.FMLPreInitializationEvent
    import org.apache.logging.log4j.Logger
    
    @Mod(modid = KotlinBasedMod.MODID, version = KotlinBasedMod.VERSION)
    class KotlinBasedMod {
       private var logger: Logger? = null
    
       @EventHandler
       fun preInit(event: FMLPreInitializationEvent) {
           logger = event.modLog
       }
    
       @EventHandler
       fun init(event: FMLInitializationEvent) {
           val test = TestKotlin(logger!!)
           test.go()
       }
    
       companion object {
           internal const val MODID = "kotlinbasedmod"
           internal const val VERSION = "0.1"
       }
    }
    

    Choix 2: écrire du Kotlin

    En Kotlin idiomatique, l'exemple de base s'écrit comme cela :

    package fr.minecraftforgefrance.tutorial
    
    import net.minecraftforge.fml.common.Mod
    
    @Mod(modid = ModTutorial.MODID)
    object ModTutorial {
       const val MODID = "tutorial"
    }
    

    Ce qui est vraiment très similaire à la version Java, à quelques exceptions près :

    • pas de point-virgules à la fin des instructions,
    • le mot-clé class est remplacé par le mot-clé object
    • pas de spécification public avant object, c'est la visibilité par défaut.
    • le type public static final String de MODID est remplacé par le plus succin const val

    Le mot-clé object est une première différence importante. On a vu que le chargement par FML cherchait une classe annotée puis l'instanciait. Kotlin a la notion de « classe instanciée une fois », et c'est exactement ce dont on a besoin ici. En remplaçant class par object, on indique au compilateur Kotlin que la classe que nous allons spécifier possède une et une seule instance, du même nom.

    Le type de MODID vaut aussi que l'on s'y arrête. Il n'est pas nécessaire de spécifier public, c'est la visibilité par défaut. Il n'est pas nécessaire de spécifier static, puisque cette classe n'a qu'une seule instance. L'immutabilité de final est remplacé par val, qui indique que la valeur suivante ne bougera pas. Quant au type, String, il est déduit par le compilateur, puisque la valeur est initialisée avec une chaîne de caractères.

    Cependant, ce code ne fonctionne pas tel quel. Nous sommes maintenant dans le monde Kotlin et le principe de classe/object n'existe pas en Java. Il faut donc donner au chargeur de FML la possibilité d'aller chercher l'instance, en lui fournissant un adaptateur de langage.

    Dans ce choix numéro 2, je vous propose d'utiliser pour cela Forgelin, qui est exactement ça : un adaptateur de langage pour Kotlin.

    Pour l'utiliser, une possibilité est de le renseigner dans le fichier build.gradle, au niveau des dépendances. :

    dependencies {
       repositories {
           maven {
               name 'Tethys'
               url 'http://tethys.drakon.io/maven'
           }
       }
       compile("io.drakon.forge:kotlin-adapter:1.0.1+1.8.9") {
           transitive = false
       }
    }
    

    À l'heure où j'écris ces lignes, la dépendance vers Forgelin amène une dépendance vers une version de log4j en conflit avec celle installée par Forge. L'option transitive = false empêche ce conflit.

    Il est alors nécessaire de relancer la génération du projet. Dans mon cas :

    gradlew idea
    

    Il est à présent possible d'indiquer à l'annotation @Mod la classe à utiliser pour l'adaptateur de langage avec le paramètre modLanguageAdapter :

    package fr.minecraftforgefrance.tutorial
    
    import net.minecraftforge.fml.common.Mod
    
    @Mod(modid = ModTutorial.MODID, modLanguageAdapter = "io.drakon.forge.kotlin.KotlinAdapter")
    object ModTutorial {
       const val MODID = "tutorial"
    }
    

    Ce qui, pour l'exemple complet, donne ceci :

    package org.puupuu.kotlinbasedmod
    
    import net.minecraftforge.fml.common.Mod
    import net.minecraftforge.fml.common.Mod.EventHandler
    import net.minecraftforge.fml.common.event.FMLInitializationEvent
    import net.minecraftforge.fml.common.event.FMLPreInitializationEvent
    import org.apache.logging.log4j.Logger
    
    @Mod(modid = KotlinBasedMod.MODID, version = KotlinBasedMod.VERSION,
           modLanguageAdapter = "io.drakon.forge.kotlin.KotlinAdapter")
    object KotlinBasedMod {
       const val MODID = "kotlinbasedmod"
       const val VERSION = "0.1"
    
       private var logger: Logger? = null
    
       @EventHandler
       fun preInit(event: FMLPreInitializationEvent) {
           logger = event.modLog
       }
    
       @EventHandler
       fun init(event: FMLInitializationEvent) {
           val test = TestKotlin(logger!!)
           test.go()
       }
    }
    

    Quelques explications sur le code et les différences avec la version Java de cette même classe :

    • j'ai déjà expliqué le concept d'object au début de la section,

    • j'ai ajouté la constante VERSION, sur le même principe que MODID,

    • l'instance de Logger est privée, comme ce n'est pas la visibilité par défaut, la visibilité private est spécifiée,

    • logger voit son type précisé après les deux-points. Contrairement aux chaînes MODID et VERSION, Kotlin a besoin d'aide car l'initialisation de la variable est null. On lui spécifie donc le type,

    • le type Logger? signifie que la variable peut-être soit de type Logger, soit null. Les types sans point d'interrogation en Kotlin ne peuvent pas contenir la valeur null.

    • le mot-clé var indique une variable, contrairement à val qui indique une valeur. Une variable est mutable : sa valeur pourra changer.

    • le mot-clé fun, déjà vu dans le tutoriel sur l'installation de l'environnement, déclare une fonction, ici fonction membre,

    • les deux points d'exclamation après logger indiquent que si logger est null, une exception de type NullPointerException sera lancée à ce niveau. Il existe d'autre mécanismes en Kotlin pour s'assurer contre ces exceptions, que nous verrons sûrement plus tard.

    • Les changements complets sur GitHub

    Choix 3: ou bien… :

    Au niveau de l'utilisation de Forgelin tout d'abord, une alternative à la dépendance maven est de cloner le dépôt, compiler l'adaptateur et mettre les jar obtenus dans le répertoire libs/ de votre Mod.

    Dans les dependencies de build.gradle, il faut alors indiquer :

    compile files("libs/kotlin-adapter-1.0.1+1.8.9.jar")
    

    Vous pouvez aussi vous passer de Forgelin et écrire votre adaptateur de langage, directement dans votre module. En ayant compris le principe de ILanguageAdapter, ce n'est pas bien compliqué. À vous d'en peser l'intérêt par rapport à l'utilisation de Forgelin.

    Résultat

    Vous avez à présent le squelette d'un module dont la classe de démarrage est en Kotlin. Ce module ne fait toujours qu'afficher un message dans le log.

    Vous avez eu un aperçu de quelques simplifications qu'apporte Kotlin par rapport à Java. Pas grand chose pour le moment, mais déjà un peu moins de verbosité.

    Crédits

    Rédaction :

    • Mokona78

    Correction :


    Ce tutoriel de Minecraft Forge France est mis à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International

    Retour vers le sommaire des tutoriels