Kotlin adventures
-
Log de la deuxième session de découverte…
Ce soir, je voulais suivre les tutoriels item, item zoom et terminer les blocs. J’aurais finalement uniquement fait celui sur les items simples. Je me suis posé pas mal de questions, j’en ai posé aussi pas mal sur Discord à robin4002 (merci encore). J’ai donc peu avancé sur du concret, mais un peu sur ma compréhension générale de Minecraft/FML. Ça compense.
Me voilà donc avec un item dans la main.
Autre chose, la signature de onItemRightClick a changé, et voici donc le code que j’ai modifié par rapport au tutoriel:
@Override public ActionResult <ItemStack>onItemRightClick(ItemStack stack, World world, EntityPlayer player, EnumHand hand) { if(!world.isRemote) { player.addChatMessage(new TextComponentString("Bonjour " + player.getName())); } return new ActionResult<ItemStack>(EnumActionResult.PASS, stack); }
-
J’ai modifié le titre en Kotlin adventures, étant donné que je vais continuer à logguer un peu mes avancées sur l’usage de Kotlin pour créer un mod.
Cette semaine a surtout été consacrée à… me battre contre Gradle. Plus exactement, comprendre comment le build système, les dépendances,… comment tout cela est agencé.
Plusieurs choses:
- Ajouter des fichiers Kotlin à un mod Java ne pose pas de réel problème. Il y a besoin d’un environnement qui peut compiler du Kotlin (comme Idea), et ajouter à Gradle le plugin ‘kotlin’ comme indiqué dans la documentation de Kotlin. Une fois compilé, Kotlin donne des .class parfaitement intégrés.
- Si l’on bootstraper le mod en Kotlin, ça se corse. Il faut utiliser un LanguageAdapter, système ajouté à la 1.8 pour aller chercher les annotations en fonction du langage utilisé (Java ou Scala par défaut). Il existe un LanguageAdapter pour Kotlin sur GitHub.
- Tout allait bien jusqu’à ce que, aujourd’hui, je me retrouve avec une nouvelle dépendance vers une nouvelle version de log4j. Je ne sais pas pourquoi, car cela reste encore un peu obscure, mais c’est a priori ma dépendance à Forgelin qui entraine la version 2.5 de log4j, qui rentre en conflit avec celle de GradleForge. Résultat, GradleStartCommon échoue lorsqu’il appel getLogger(). L’avantage, c’est que c’est rapide à voir, c’est au tout début du lancement.
Finalement, j’ai choisi d’utiliser une compilation locale d’un jar pour Forgelin, que je mets en dépendance locale.
Et j’ai donc un Mod Kotlin qui se lance (du moins avec runClient car avec une opération récente, depuis idea, le mod n’est plus trouvé…)
-
Qu’est ce que Forgelin ? Une version Forge pour Kotlin ?
Envoyé de mon Nexus 4 en utilisant Tapatalk
-
Forgelin est une implémentation du ILanguageAdapter pour Kotlin.
Dans les grandes lignes, ça implémente la découverte de l’annotation @Mod pour donner l’objet de démarrage à FML.
Dans la version Java de ILanguageAdapter, il y a une bête recheche de l’annotation @Mod sur une classe, puis un appel de createInstance() sur le type.
En Kotlin, il existe la notion de “object” en plus de “class”. Un “object” est une classe à instance unique. Forgelin implémente la recherche sur cet objet et récupère son instance (pas besoin de createInstance() puisque Kotlin s’en charge déjà).
-
Cette semaine, j’ai trouvé une idée de Mod qui me permettra plus efficacement de comprendre les rouages du Mod avec Forge. J’ai donc commencé par prendre des notes puis je les ai regroupées pour me donner un plan de développement et un aperçu de ce sur quoi je dois me renseigner (un paquet de trucs).
Je me suis battu avec une gestion de dépendance automatique qui rentrait en conflit avec une autre dépendance… J’adore tellement ça. Au final, j’ai gagné. Je pense. Au passage, j’ai créé un Pull Request sur Forgelin puisqu’il me semble qu’il y a une erreur (surtout, ce sont les logs de Minecraft qui râlent).
Je suis reparti de la création d’un module à vide, mais en adaptant directement à Kotlin, pour m’assurer que j’avais bien compris ce que je faisais. J’ai ajouté un CreativeTabs, un Bloc et deux Items. Le passage à Kotlin demande parfois de se poser des questions sur comment adapter le code Java avec les outils Kotlin.
Par exemple, Kotlin permet les méthodes d’extension. Ce principe, qui existe par exemple en C#, en Objective C (et d’autres encore), permet d’ajouter à une classe définie de nouvelles méthodes. Et cette classe peut tout à fait résider dans un package que l’on ne peut pas modifier.
Ainsi, pour enregistrer un item, j’ai défini la méthode d’extension suivante :
fun Item.register(name: String) { setRegistryName(MonMod.MODID, name) unlocalizedName = registryName.toString() GameRegistry.register(this) }
Qui me permet ensuite d’écrire sur une instance d’Item quelque chose d’aussi simple que :
item.register("my_item_name")
Et pour terminer la semaine et consolider les connaissances acquises, j’ai écrit ceci : Configurer l’espace de travail de Forge pour Kotlin
-
Le tutoriel écrit la semaine dernière avait été induit pas l’idée d’un tutoriel sur la création de la base d’un mod et d’un item, mais façon Kotlin.
Après avoir installé les outils, il restait donc à :
deux tutoriaux qui m’ont pris chacun une bonne grosse soirée à chaque fois, pour reprendre sur le mod de tutoriel ce que j’avais appris, et après vérifications, expliquer le pourquoi des choses.
J’ai enchaîné sur à nouveau de l’étude : comprendre les BlockStates. J’étais parti sur de fausses idée sur le fonctionnement de Minecraft. En cette fin de semaine, j’ai compris pas mal de chose sur la conception de Minecraft, j’ai pu donc créer un block un peu dynamique, avec changement de propriété par rapport au temps qui passe.
Pour mon étape suivante, je suis parti sur l’idée d’ajouter à mon environnement de quoi lancer des tests. De manière générale, j’aime travailler à partir de tests. Je suis arrivé à écrire un test avec le framework kotlintest, le compiler et le lancer… mais c’est tombé en marche un peu trop magiquement pour moi.
Je vais devoir reprendre ça calmement, et pourquoi pas assurer ça par un tutoriel
-
Cette semaine à donc commencé par la mise en place sur mon projet de KotlinTest, qui permet d’utiliser des tests lorsque l’on programme. J’ai prévu une petite introduction au principe plus tard pour le forum.
Premiers essais, évidemment, voir si Minecraft est coopératif. Comme je pouvais m’en douter, non. Ça doit être possible, mais le moindre accès à un block nécessite que le Bootstrap ait été appelé, et l’appeler (ce qui est déjà assez long) sans prévenir Forge fait râler Forge.
Bref, pas grave, je limiterai mes tests à mes classes indépendantes. Pour ce qui touche à des classes de Minecraft ou de Forge, le bon test à la main sera de rigueur.
Grâce à ça, j’ai pu avancer dans mon mod, en faisant s’animer des formes constituées de blocs dans le monde. Ça marche bien, même si je pense que ça doit pouvoir s’optimiser d’un point de venu des demandes de mises à jour auprès du monde.
Au passage, j’ai ajouté deux extensions Kotlin sur Vec3i, ce qui me permet d’écrire naturellement des additions et des soustractions les vecteurs.
val shift = _origin - value
J’ai aussi écrit une extension au niveau des blocks pour faciliter le fait d’ignorer des propriétés au niveau des variantes.
val monBlock = MonBlock().apply { setCreativeTab(tab) ignoreProperty(proxy, COUNT_PROPERTY) }
-
Très petite semaine que cette semaine. Deux déplacements et un ordinateur trop léger pour modder.
Du coup, sur ma liste des choses à faire, j’avais “ajouter une commande” pour contrôler mon mod. Je me suis dit que c’était jouable en peu de temps. Et grâce à ce tutoriel, ce fut le cas. Il nécessite un peu d’adaptation pour fonctionner en 1.9, mais rien de bien méchant.
Comme je n’ai pas grand chose à dire sur mes avancées Forge en particulier, je vais me permettre un conseil en programmation de manière générale.
En développant ma commande, je me retrouve avec une petite classe, dense, et pourtant, avec ceci en préambule :
import net.minecraft.block.Block import net.minecraft.block.material.Material import net.minecraft.command.CommandBase import net.minecraft.command.ICommandSender import net.minecraft.command.WrongUsageException import net.minecraft.server.MinecraftServer import net.minecraft.util.ResourceLocation import net.minecraft.util.math.BlockPos import net.minecraft.util.text.TextComponentTranslation import net.minecraft.world.World import net.minecraftforge.fml.common.registry.ForgeRegistries
C’est long… très long, et très varié, trop varié. Une cloche d’alerte sonne dans ma tête et j’ai ajouté sur ma TODO liste une étape de nettoyage.
Un préambule de cette taille m’indique une chose : la classe qui gère ma commande a trop de responsabilités, elle est dépendante de trop de systèmes. Et plus une classe a de responsabilités, plus elle a de dépendance, plus elle va être complexe à maintenir et à faire évoluer.
Le nettoyage dont je parle est une vraie réflexion sur ce qu’est la responsabilité de ma commande et ce qui devrait être gérée par une autre classe, pas juste enlever des import au hasard (ils sont tous utiles à ma classe, ici).
-
Bof, je ne trouve pas cela très long, déjà pour ce qui est de CommandBase, ICommandSender et WrongUsageException c’est le minimum pour une commande, ensuite TextComponentTranslation pour supporter le multi-langue c’est aussi un basique. Reste 7 imports après cela, World, Block, Material et BlockPos pour accéder au bloc dans le monde et ForgeRegistries pour avoir le bloc enregistré dans Forge. Reste MinecraftServer et ResourceLoaction, après je ne sais pas ce que fait ta commande mais bon, 11 imports c’est peu. Sinon je trouve que tu fait du beau boulot, j’aime bien ton topic Kotlin adventure
-
Merci pour le compliment.
En effet, quoi qu’il arrive, le monde Java est un monde bavard en import, et Kotlin n’y peut rien. En regardant mes autres classes, j’en ai beaucoup avec 7 imports qui ne peuvent pas être descendus sans rendre le code trop contraint.
Ici, à vue de nez, je me dis que Block, Material et ForgeRegistries devraient aller ailleurs. BlockPos et World… peut-être. Ce qui amène entre 6 et 8 import, qui est plus dans l’incompressible d’après ce que je vois.
À vrai dire, il n’y a pas que le nombre d’imports qui lance l’alerte. Je fais dans la commande quelque chose qu’une autre partie du mod fait déjà. J’ai donc deux parties avec un code similaire. En factorisant les deux parties, cela aura pour effet d’enlever les besoins à Block, Materiel et ForgeRegistries (et peut-être BlockPos et World, je verrai ça quand je le ferai).
-
Cette semaine, j’ai creusé des piscines !
Mais avant toute chose, je m’étais promis de réduire le nombre de dépendances de ma commande. Ce que j’ai fait en séparant correctement les responsabilités : la commande… gère la commande. Mais ne fait pas l’action. L’action est déléguée à un autre object (globalement équivalent d’une classe statique en Java), qui se charge de l’action.
Cette action étant aussi utilisée par l’activation d’un bloc par RedStone, elle est utilisable des deux endroits.
Cela enlève 5 dépendances, je tombe à 6, et je suis content.
Au passage, j’ai utilisé un pattern plutôt sympathique pour remplacer quelque chose qui ressemble à ça :
if (jePeuxFaireUnTruc()) { jeFaisLeTruc(); } else { afficheUnMessageDErreurSurLeChatDuSender() }
Je n’aime pas trop car cela demande à la commande de connaître un fonctionnement qui n’est pas de son ressort. La commande est là pour lancer le Truc, elle n’a pas à s’occuper de savoir si c’est possible ou pas.
D’un autre côté, ce n’est pas à l’action Truc de savoir comment afficher un message dans le Chat si cette action n’a rien à voir avec le Chat. Il faudrait lui passer le sender, objet dont l’action n’a probablement pas besoin.
Alors il y a le classique :
if (!jeFaisLeTruc()) { afficheUnMessageDErreurSurLeChatDuSender() }
… que je n’aime pas. Quand une fonction à un nommage impératif, elle effectue une action. Elle n’a pas à renvoyer de valeur. De plus, à la lecture, on loupe vite la négation.
On peut aussi utiliser une exception. Mais c’est sur dimensionné et inadapté dans ce cas là. Ne pas pouvoir faire l’action n’est probablement pas un cas d’exception. Et l’appelant va devoir gérer des exceptions, ce qui est un peu lourd.
En Kotlin, j’ai donc écrit cela:
faisLeTrucOuBien(paramètres) { afficheUnMessageDErreurSurLeChatDuSender() }
C’est presque comme la version précédente avec le if(!..), mais la fonction indique maintenant clairement qu’elle à un cas d’échec, et le bloc de code d’échec est indiqué juste après, dans une fonction anonyme, qui embarque le sender.
La fonction appelée peut donc vérifier si elle peut faire le Truc, le faire si c’est possible, et sinon appeler la lambda.
Voilà pour la première partie.
Dans la seconde partie, donc, j’ai creusé des piscines.
Le but pour moi était de jouer avec la génération/modification du monde. Pas beaucoup de complexité dans cette étape étant donné que j’avais déjà vu les différents concepts. J’ai pu quand même aller plus en profondeur dans la compréhension du code de Minecraft et de sa gestion de blocks, tileEntity, states,… merci à robin4002 pour quelques éclaircissements sur Discord.
J’en ai profité pour me faire un petit outil en Kotlin pour traverser un ensemble de positions du monde. Ce que je voulais éviter (et que j’ai écrit en premier lieu) était un classique :
for(z in 1..100) { for(y in 1..100) { for(x in 1..100) { val position = Vec3i(x, y, z) .... } } }
Les boucles sont un peu différentes qu’en Java, mais la syntaxe se devine aisément.
Ces doubles ou triples boucles, on les retrouves souvent quand on veut faire une opération sur un ensemble de blocs d’un zone. J’avais déjà un outil pour faire une opérations sur un ensemble de coordonnées données. Je m’en suis donc inspiré et je peux maintenant écrire.
val firstCorner = generationPosition.west(20).north(20) val secondCorner = generationPosition.east(20).south(20).up(20) val zone = ZoneVisitor(firstCorner, secondCorner) for (position in zone) { val blockPosition = BlockPos(position) when (position) { in zone.floor, in zone.walls -> world.setBlockState(blockPosition, diorite) else -> world.setBlockToAir(blockPosition) } }
Les membres floor et walls sont des ‘Matchers’ qui répondent true si la coordonnée est sur le sol ou sur un des murs. Plutôt que de faire des tests de plusieurs coordonnées directement ici, elles sont faites dans les matchers. Cela évite le copier/coller, les erreurs bêtes et donne un code plus compact et plus simple à lire.
ZoneVisitor a bien entendu été développé en TDD (cf Utiliser un framework de test en Kotlin) ce qui m’a permis de le développer, tester, valider assez rapidement, sans lancer le client Minecraft.
Et voilà une belle piscine.
-
Pas spécifiquement de Kotlin aujourd’hui. Cet été, après une pause, j’ai redémarré la programmation de mon mod. Comme il n’y pas spécifiquement de nouveautés côté Kotlin, parlons d’un autre outil.
Pour mon développement, j’utilise Freeplane pour le concept de Mindmap. Il existe plein d’autres logiciels de Mindmap, j’aime bien celui là pour son côté très complet (par contre, il y en a des plus jolis).
Je tiens à jour la carte en fur et à mesure des développements et des idées qui viennent. En début de programmation, j’y ai jeté en vrac les idées qui me venaient, puis j’ai arrangé le tout sous forme de “user stories”, c’est-à-dire de petit scénarios, du point de vue utilisateur (joueur).
Ces user stories ont provoqué d’autres branches avec ce qu’il y avait à faire, d’un point de vue haut niveau d’abord, puis séparé en tâches plus concises. Lorsqu’il me manque une connaissance, je le note dans une branche. Lorsque je trouve de la documentation qui me permettra d’avancer sur un point, je créé un lien.
Cette carte heuristique est dynamique. Lorsque j’ai terminé quelque chose, j’ajoute une icône de coche verte. Lorsque je décide de ne pas faire quelque chose, j’ajoute une icône pour le signifier (comme ça, je me souviens plus tard pourquoi je ne l’ai pas fait).
Il arrive aussi que, en avançant, certaines choses que j’avais prévues n’aient plus de sens, ou bien je comprends que techniquement, ce n’est pas comme cela que l’on doit faire. Pareil, je mets à jour.
Si une idée me vient alors que je suis sur un autre point, cela m’aide à rester concentrer. Je ne pars pas sur un autre truc, je le note.
Par exemple, hier je vérifiais les coordonnées d’un joueur pour savoir s’il était dans une zone particulière. J’étais en train de construire mon système quand tout à coup, je réalise qu’il faut aussi vérifier que le joueur est dans le bon monde, les coordonnées ne suffisent pas.
Seulement, j’étais en train de travailler sur les coordonnées, et je n’avais pas de gestion du monde. J’ai donc ajouté une sous branche : vérifier le monde. J’ai pu continuer, terminer ma tâche sur les coordonnées, la soumettre au gestionnaire de version, puis passer à la vérification du monde.
Ce qui m’amène à parler aussi de deux règles de programmation que je me fixe.
-
lorsque je démarre une tâche, je la complète et je la soumet au gestionnaire de version dans un état fonctionnel. Interdiction d’archiver des fonctions non appelées, des classes qui ne servent à rien, des trucs que je sais buggés (les bugs non intentionnels, c’est autre chose ::)
-
si lorsque j’avance dans la tâche je me retrouve bloqué car il manque une autre partie (un système que je n’avais pas prévu et dont j’ai besoin), je m’arrête, je note le besoin sur la mindmap, je revert ce que je faisais (oui oui, j’efface tout !), puis je pars sur le nouveau besoin (éventuellement je fais ça récursivement).
La méthode qui consiste à effacer ce que l’on vient de faire peut sembler violente. En fait, elle est plutôt saine. Ce code qui a été écrit et qui a besoin d’un autre service pose des contraintes sur ce nouveau services. Soit il limite son développement, soit il ne le limite pas et le code non effacé ne fonctionnera pas. Donc, autant l’effacer, il ne gêne plus.
Cela implique aussi une division en petites tâches, et un archivage petite tâche par petite tâche, dans un état toujours fonctionnel. Ainsi, effacer ce que l’on vient de faire et revenir au “checkpoint” précédent n’est pas si terrible. C’étaient probablemet une dizaine de lignes.
Et vous, avez vous certaines règles de méthodes de programmation ?
-
-
@‘Mokona78’:
Pas spécifiquement de Kotlin aujourd’hui. Cet été, après une pause, j’ai redémarré la programmation de mon mod. Comme il n’y pas spécifiquement de nouveautés côté Kotlin, parlons d’un autre outil.
Pour mon développement, j’utilise Freeplane pour le concept de Mindmap. Il existe plein d’autres logiciels de Mindmap, j’aime bien celui là pour son côté très complet (par contre, il y en a des plus jolis).
Je tiens à jour la carte en fur et à mesure des développements et des idées qui viennent. En début de programmation, j’y ai jeté en vrac les idées qui me venaient, puis j’ai arrangé le tout sous forme de “user stories”, c’est-à-dire de petit scénarios, du point de vue utilisateur (joueur).
Ces user stories ont provoqué d’autres branches avec ce qu’il y avait à faire, d’un point de vue haut niveau d’abord, puis séparé en tâches plus concises. Lorsqu’il me manque une connaissance, je le note dans une branche. Lorsque je trouve de la documentation qui me permettra d’avancer sur un point, je créé un lien.
Cette carte heuristique est dynamique. Lorsque j’ai terminé quelque chose, j’ajoute une icône de coche verte. Lorsque je décide de ne pas faire quelque chose, j’ajoute une icône pour le signifier (comme ça, je me souviens plus tard pourquoi je ne l’ai pas fait).
Il arrive aussi que, en avançant, certaines choses que j’avais prévues n’aient plus de sens, ou bien je comprends que techniquement, ce n’est pas comme cela que l’on doit faire. Pareil, je mets à jour.
Si une idée me vient alors que je suis sur un autre point, cela m’aide à rester concentrer. Je ne pars pas sur un autre truc, je le note.
Par exemple, hier je vérifiais les coordonnées d’un joueur pour savoir s’il était dans une zone particulière. J’étais en train de construire mon système quand tout à coup, je réalise qu’il faut aussi vérifier que le joueur est dans le bon monde, les coordonnées ne suffisent pas.
Seulement, j’étais en train de travailler sur les coordonnées, et je n’avais pas de gestion du monde. J’ai donc ajouté une sous branche : vérifier le monde. J’ai pu continuer, terminer ma tâche sur les coordonnées, la soumettre au gestionnaire de version, puis passer à la vérification du monde.
Ce qui m’amène à parler aussi de deux règles de programmation que je me fixe.
-
lorsque je démarre une tâche, je la complète et je la soumet au gestionnaire de version dans un état fonctionnel. Interdiction d’archiver des fonctions non appelées, des classes qui ne servent à rien, des trucs que je sais buggés (les bugs non intentionnels, c’est autre chose ::)
-
si lorsque j’avance dans la tâche je me retrouve bloqué car il manque une autre partie (un système que je n’avais pas prévu et dont j’ai besoin), je m’arrête, je note le besoin sur la mindmap, je revert ce que je faisais (oui oui, j’efface tout !), puis je pars sur le nouveau besoin (éventuellement je fais ça récursivement).
La méthode qui consiste à effacer ce que l’on vient de faire peut sembler violente. En fait, elle est plutôt saine. Ce code qui a été écrit et qui a besoin d’un autre service pose des contraintes sur ce nouveau services. Soit il limite son développement, soit il ne le limite pas et le code non effacé ne fonctionnera pas. Donc, autant l’effacer, il ne gêne plus.
Cela implique aussi une division en petites tâches, et un archivage petite tâche par petite tâche, dans un état toujours fonctionnel. Ainsi, effacer ce que l’on vient de faire et revenir au “checkpoint” précédent n’est pas si terrible. C’étaient probablement une dizaine de lignes.
Et vous, avez vous certaines règles de méthodes de programmation ?
La dernière fois que j’ai eu a utiliser du mind mapping, c’était lors de mon cursus universitaire ^^
Habituellement je ne le fais pas, je divise mes taches en sous-tâche et je ne fais qu’une tache a la fois.
Lorsque les projets ne sont pas trop complexe, j’arrive a suivre correctement les choses et je n’en ai pas besoin pour le développement.
En revanche, je reconnais que ça pourrais me servir de documentation technique, mais lorsque je mod, c’est quelque chose que j’ai tendance a oublier volontairement puisque je suis généralement le seul a travailler dessus.Pour le versionnage, je me fixe plus ou moins les même contraintes.
Je comit dès que j’ai terminé un bout de code et je ne push que quand l’ensemble de mes comits forment un tout cohérent et fonctionnel.
Comme ça, j’ai aussi mes checkpoints. Pareillement, quand quelque chose ne me plait pas, je n’hésite pas a tout recommencer. -
-
@‘RedRelay’:
En revanche, je reconnais que ça pourrais me servir de documentation technique, mais lorsque je mod, c’est quelque chose que j’ai tendance a oublier volontairement puisque je suis généralement le seul a travailler dessus.
Je me méfie de mon moi de la semaine dernière, je le considère comme une personne différente et parfois, je trouve qu’il a fait n’importe quoi, ou bien je ne comprends pas ce qu’il a fait. Du coup, je préfère me laisser des notes