|
| 1 | +--- |
| 2 | +sidebar_position: 2 |
| 3 | +title: Dist Executor |
| 4 | +description: Comment utiliser les dist executors ? |
| 5 | +tags: [avancé] |
| 6 | +--- |
| 7 | + |
| 8 | +## Introduction |
| 9 | +Le système de `Dist Executor` est une API efficace fournie par **FML** située dans le projet `fmlcore` permettant de gérer le code ne devant s'exécuter que sur une `Dist` particulière. Ce système a été ajouté en remplacement du système de `SidedProxy`, présent lors des anciennes versions de **Forge** (1.12.2 et avant). |
| 10 | + |
| 11 | + |
| 12 | +## C'est quoi une `Dist` ? |
| 13 | +Une `Dist` représente sur quel "côté", sur quel distribution de Minecraft doit s'exécuter ce code. Ces distributions sont représentées dans l'énumération `net.minecraftforge.api.distmarker.Dist`. Aujourd'hui, il existe 2 distributions : |
| 14 | +- `CLIENT` : La distribution du client. Il s'agit du client avec lequel les joueurs jouent. Il gère la partie rendu/graphique du jeu. |
| 15 | +- `DEDICATED_SERVER` : La distribution du serveur dédié. Il s'agit de la distribution réservée aux serveurs. Il gère le monde ainsi que quelques éléments logiques, et communique avec le client via le réseau. Il ne contient aucun élément visuel du jeu. |
| 16 | + |
| 17 | +:::caution |
| 18 | +La distribution `DEDICATED_SERVER` n'est pas utilisée lors de l'exécution du serveur intégré lancé en solo. |
| 19 | +::: |
| 20 | + |
| 21 | + |
| 22 | +## L'annotation `@OnlyIn` |
| 23 | +L'annotation `@OnlyIn` permet d'indiquer à **FML** de charger ou non le membre annoté en fonction de la `Dist` spécifiée en paramètre. Tout comme l'énumération `Dist`, elle se situe dans le package `net.minecraftforge.api.distmarker`. Elle peut-être utilisée sur les classes, les champs, les méthodes, les constructeurs, les packages et les annotations. Pour information, cette annotation est traitée dans la classe `RuntimeDistCleaner` du projet `fmlloader`. L'annotation `@OnlyIns` ne sera pas traité dans ce tutoriel. |
| 24 | + |
| 25 | +Si on tente d'appeler un membre depuis une autre `Dist`, le membre sera considéré comme inexistant et des erreurs comme `NoSuchFieldError`, `NoSuchMethodError` ou encore `ClassNotFoundException` peuvent survenir en fonction du type du membre. |
| 26 | + |
| 27 | +Par exemple : |
| 28 | +```java |
| 29 | +package fr.lmf.distexecutor; |
| 30 | + |
| 31 | +import net.minecraftforge.api.distmarker.Dist; |
| 32 | +import net.minecraftforge.api.distmarker.OnlyIn; |
| 33 | + |
| 34 | +@OnlyIn(Dist.CLIENT) |
| 35 | +public class OnlyClientClass { |
| 36 | + |
| 37 | + private Object aField; |
| 38 | + |
| 39 | + public void aMethod() { |
| 40 | + // do something |
| 41 | + } |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +La classe ne sera chargée que sur le client. Si elle est appelée sur le serveur, une erreur sera propagée. |
| 46 | + |
| 47 | +En revanche, une même classe par exemple peut contenir des membres reliés à des `Dist`s différentes. |
| 48 | +Exemple : |
| 49 | +```java |
| 50 | +package fr.lmf.distexecutor; |
| 51 | + |
| 52 | +import net.minecraftforge.api.distmarker.Dist; |
| 53 | +import net.minecraftforge.api.distmarker.OnlyIn; |
| 54 | + |
| 55 | +public class SimpleClass { |
| 56 | + |
| 57 | + @OnlyIn(Dist.CLIENT) |
| 58 | + private Object aField; |
| 59 | + |
| 60 | + @OnlyIn(Dist.DEDICATED_SERVER) |
| 61 | + public void aMethod() { |
| 62 | + // do something |
| 63 | + } |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +Ici, la classe sera chargée, quelque soit la distribution, mais le field `aField` sera inexistant sur serveur et de même pour la méthode `aMethod` sur le client. Si nous exécutons `System.out.println(this.aField);` dans la méthode `aMethod`, le jeu plantera. |
| 68 | + |
| 69 | + |
| 70 | +## La classe `DistExecutor` |
| 71 | +Maintenant, vous aimeriez peut-être savoir comment appeler une classe, une méthode ou quoi que ce soit en fonction de la `Dist` pour éviter les erreurs évoquées plus haut ? |
| 72 | +Utiliser la reflection pour voir si la classe `net.minecraft.client.Minecraft` (uniquement présente sur le client) existe serait une solution ; hélas les limitations de **FML** nous en empêche : une erreur est propagée et ferme le jeu automatiquement avant même que nouis puissons exécuter du code. De toute façon, ce n'est pas la méthode propre et recommandée que nous recommande **Forge**. |
| 73 | + |
| 74 | +La classe `DistExecutor` entre maintenant en jeu. Elle se situe dans le package `net.minecraftforge.fml` du projet `fmlcore`. Elle possède quelques méthodes statiques utilitaires qui peuvent répondr à notre problématique. |
| 75 | +Nous nous intéresserons pour le moment qu'aux méthodes `(un)safeRunForDist` et `(un)safeRunWhenOn`. Libre à vous de lire la JavaDoc disponible dans la classe pour connaître l'utilité de chaque méthode. Veillez à ne pas utiliser - du moins le moins possible - les méthodes annotées avec l'annotation `@Deprecated`. |
| 76 | + |
| 77 | +:::caution |
| 78 | +Les méthodes `unsafe` n'exécutent pas certaines vérifications que les méthodes `safe` appliquent à l'exécution du jeu. Nonobstant, ces vérifications ne sont pas appliquées en production, quand vous lancer le jeu depuis un launcher par exemple. Vous pouvez donc avoir un plantage en lançant le jeu depuis un environnement de développement, et pas en lançant votre jeu de manière classique. Enfin, les méthodes `unsafe` ne peuvent prévenir de certaines erreurs comme les `ClassCastException`. |
| 79 | +::: |
| 80 | + |
| 81 | +### La méthode `(un)safeRunForDist` |
| 82 | +La méthode `(un)safeRunForDist` permet de retourner une instance de la classe demandée en paramètre en fonction de la `Dist`. Par exemple, un système de "proxy" est facilement reproductible grâce à cette méthode : |
| 83 | +```java |
| 84 | +package fr.lmf.distexecutor; |
| 85 | + |
| 86 | +public interface SidedManager { |
| 87 | + void init(); |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +Cette interface va nous permettre de définir un membre commun entre nos `Manager`s : un pour le client, et l'autre pour le serveur. Voici un exemple d'implémentation pour le client : |
| 92 | +```java |
| 93 | +package fr.lmf.distexecutor.client; |
| 94 | + |
| 95 | +import fr.lmf.distexecutor.SidedManager; |
| 96 | +import net.minecraftforge.api.distmarker.Dist; |
| 97 | +import net.minecraftforge.api.distmarker.OnlyIn; |
| 98 | +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; |
| 99 | +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; |
| 100 | + |
| 101 | +@OnlyIn(Dist.CLIENT) |
| 102 | +public class ClientManager implements SidedManager { |
| 103 | + |
| 104 | + @Override |
| 105 | + public void init() { |
| 106 | + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientSetup); |
| 107 | + } |
| 108 | + |
| 109 | + public void clientSetup(FMLClientSetupEvent event) { |
| 110 | + // do something at client startup |
| 111 | + } |
| 112 | +} |
| 113 | +``` |
| 114 | +En voici une autre pour le serveur : |
| 115 | +```java |
| 116 | +package fr.lmf.distexecutor.server; |
| 117 | + |
| 118 | +import fr.lmf.distexecutor.SidedManager; |
| 119 | +import net.minecraftforge.api.distmarker.Dist; |
| 120 | +import net.minecraftforge.api.distmarker.OnlyIn; |
| 121 | +import net.minecraftforge.common.MinecraftForge; |
| 122 | +import net.minecraftforge.eventbus.api.SubscribeEvent; |
| 123 | +import net.minecraftforge.fmlserverevents.FMLServerStartedEvent; |
| 124 | + |
| 125 | +@OnlyIn(Dist.DEDICATED_SERVER) |
| 126 | +public class ServerManager implements SidedManager |
| 127 | +{ |
| 128 | + @Override |
| 129 | + public void init() { |
| 130 | + MinecraftForge.EVENT_BUS.register(this); |
| 131 | + } |
| 132 | + |
| 133 | + @SubscribeEvent |
| 134 | + public void onServerStart(FMLServerStartedEvent event) { |
| 135 | + // do something at server startup |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | +Enfin, il faudra exécuter la bonne méthode `init` au démarrage du mod. La méthode `(un)safeRunForDist` prend 2 paramètres à signature identiques : un [`Supplier`](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/util/function/Supplier.html) d'un `(Safe)Supplier` de votre classe cible (ici `ClientManager` ou `ServerManager`). Un `SafeSupplier` est une interface fournie par **FML** étandant `Supplier` et `SafeReferent`. Un `SafeReferent` est une interface elle aussi fournie par **FML** qui va subir des vérifications et propager une erreur si il n'est pas jugé "safe". Les méthodes `unsafe` ne demandent pas de `SafeSupplier`, remplacé par un `Supplier` classique. |
| 140 | + |
| 141 | +```java |
| 142 | +package fr.lmf.distexecutor; |
| 143 | + |
| 144 | +import fr.lmf.distexecutor.client.ClientManager; |
| 145 | +import fr.lmf.distexecutor.server.ServerManager; |
| 146 | +import net.minecraftforge.fml.DistExecutor; |
| 147 | +import net.minecraftforge.fml.common.Mod; |
| 148 | + |
| 149 | +@Mod("distexecutorexample") |
| 150 | +public class DistExecutorMod { |
| 151 | + |
| 152 | + // some fields and constants |
| 153 | + |
| 154 | + public DistExecutorMod() { |
| 155 | + // do something |
| 156 | + var manager = DistExecutor.unsafeRunForDist(() -> ClientManager::new, () -> ServerManager::new); |
| 157 | + manager.init(); |
| 158 | + // do something |
| 159 | + } |
| 160 | + |
| 161 | + // other methods |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +:::tip |
| 166 | +Notez l'utilisation du mot-clé `var`, introduit dans **Java** depuis la version 10. Il détectera automatiquement le type commun de nos deux classes, ici `SidedManager`. Nous avons donc accès aux méthodes dans cette classe, soit `init` dans le cadre de l'exemple, libre à vous d'en rajouter autant que vous voulez pour les usages de votre choix. |
| 167 | +::: |
| 168 | +
|
| 169 | +### La méthode `(un)safeRunWhenOn` |
| 170 | +Voici une seconde méthode qui fonctionne un peu différemment, rassurez vous, vous n'avez pas besoin de tout recommencer, gardez vos classes `ClientManager` et `ServerManager`, vous allez en avoir besoin. |
| 171 | + |
| 172 | +La méthode `(un)safeRunWhenOn` fonctionne différemment, déjà, elle ne prend pas un ensemble de `Supplier`, mais une `Dist` en premier paramètre et un `Supplier` d'un objet `(Safe)Runnable` (en fonction de si vous utilisez la méthode safe ou unsafe). Si la `Dist` fournie en paramètre correspond à la distribution actuelle, le code contenu dans l'objet `(Safe)Runnable` sera exécuté. Par exemple, voici un code qui affichera dans la console "Bonjour depuis le client" sur le client et "Bonjour depuis le serveur" sur le serveur : |
| 173 | + |
| 174 | +```java |
| 175 | +DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> System.out.println("Bonjour depuis le client")); |
| 176 | +DistExecutor.unsafeRunWhenOn(Dist.DEDICATED_SERVER, () -> () -> System.out.println("Bonjour depuis le serveur")); |
| 177 | +``` |
| 178 | + |
| 179 | +Jusqu'ici, nos deux `Manager`s avait la même méthode en commun, appelée au même moment. En revanche, vous aimeriez pouvoir être plus libre dans l'utilisation de vos `Manager`s en ajoutant des méthodes indépendantes et pouvant être appelées un peu partout comme pouvoir démarrer une base de donnée depuis le serveur, ou alors ouvrir un écran depuis le client... |
| 180 | + |
| 181 | + On considère une méthode `foo(String)` dans `ClientManager` et une méthode `bar(int)` dans `ServerManager`. Effectivement, polymorphisme et héritage ici ne seront pas utiles. Une solution est de déclarer 2 fields publiques et statiques (ou alors privé, avec un accesseur), un pour le `ClientManager` et l'autre pour le `ServerManager`, de les initialiser chacun à l'aide de la méthode `(un)safeRunWhenOn`. Puis ensuite de les appeler quand bon vous semble dans une classe elle-même annotée `@OnlyIn` avec la `Dist` correspondante, ou bien en utilisant à nouveau la méthode `(un)safeRunWhenOn`. |
| 182 | + |
| 183 | +Vous pouvez également profiter de l'interface `SidedManager` créée plus tôt pour donner un accès sûr aux méthodes communes et publiques des deux `Manager`s : |
| 184 | +```java |
| 185 | +package fr.lmf.distexecutor; |
| 186 | +
|
| 187 | +import fr.lmf.distexecutor.SidedManager; |
| 188 | +import fr.lmf.distexecutor.client.ClientManager; |
| 189 | +import fr.lmf.distexecutor.server.ServerManager; |
| 190 | +import net.minecraftforge.api.distmarker.Dist; |
| 191 | +import net.minecraftforge.api.distmarker.OnlyIn; |
| 192 | +import net.minecraftforge.fml.DistExecutor; |
| 193 | +import net.minecraftforge.fml.common.Mod; |
| 194 | +
|
| 195 | +@Mod("distexecutorexample") |
| 196 | +public class DistExecutorMod { |
| 197 | +
|
| 198 | + @OnlyIn(Dist.CLIENT) |
| 199 | + private static ClientManager clientManager; |
| 200 | +
|
| 201 | + @OnlyIn(Dist.DEDICATED_SERVER) |
| 202 | + private static ServerManager serverManager; |
| 203 | +
|
| 204 | + private static SidedManager currentManager; |
| 205 | +
|
| 206 | + public DistExecutorMod() { |
| 207 | + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { |
| 208 | + clientManager = new ClientManager(); |
| 209 | + clientManager.foo("foobar"); |
| 210 | + }); |
| 211 | + DistExecutor.unsafeRunWhenOn(Dist.DEDICATED_SERVER, () -> () -> { |
| 212 | + serverManager = new ServerManager(); |
| 213 | + serverManager.bar(0); |
| 214 | + }); |
| 215 | + currentManager = DistExecutor.unsafeRunForDist(() -> DistExecutorMod::getClientManager, () -> DistExecutorMod::getServerManager); |
| 216 | + currentManager.init(); |
| 217 | + } |
| 218 | +
|
| 219 | + @OnlyIn(Dist.CLIENT) |
| 220 | + public static ClientManager getClientManager() { |
| 221 | + return clientManager; |
| 222 | + } |
| 223 | +
|
| 224 | + @OnlyIn(Dist.DEDICATED_SERVER) |
| 225 | + public static ServerManager getServerManager() { |
| 226 | + return serverManager; |
| 227 | + } |
| 228 | +
|
| 229 | + public static SidedManager getCurrentManager() { |
| 230 | + return currentManager; |
| 231 | + } |
| 232 | +} |
| 233 | +``` |
| 234 | +
|
| 235 | +## Conclusion |
| 236 | +Vous savez maintenant vous servir de l'annotation `@OnlyIn` et de la classe `DistExecutor`. Vous êtes au courant des erreurs qui peuvent survenir si vous utilisez de manière incorrecte ces classes et que vous appelez de manière non vérifiée des membres présent sur une seule distribution du client. |
| 237 | + |
| 238 | +Ce n'est pas une notion évidente, c'est pour ça que j'ai essayé d'être le plus clair et concis et de donner quelques exemples et quelques tips. Toutefois il existe évidemment d'autres manières d'utiliser ces outils pratique. Ne vous découragez pas au moindre plantage et faites attention à ce que vous appelez. |
0 commit comments