|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +title: Capabilities |
| 4 | +description: Comment utiliser les capabilities ? |
| 5 | +tags: [avancé] |
| 6 | +--- |
| 7 | + |
| 8 | +Les `capabilities` sont un système mis à disposition par Forge permettant de stocker des données sur des **BlockEntities**(TileEntities), des **Entities**, des **ItemStacks**, des **Levels**(Worlds) et des **LevelChunks**(Chunks). |
| 9 | + |
| 10 | +## Utiliser une Capability |
| 11 | + |
| 12 | +Forge fournit par défaut trois capabilities : `IItemHandler`, qui permet de stocker des items, `IFluidHandler`, qui permet de stocker des liquides et enfin `IEnergyStorage`, qui permet de stocker de l'énergie. |
| 13 | + |
| 14 | +Une capability possède au minimum normalement trois classes : l'interface(Exemple : `IItemHandler`), l'(les) implémentation(s) par défaut de la capability(Exemple : `ItemStackHandler`) et enfin la classe qui contient l'instance de la capability et qui sert à l'enregistrer(Exemple : `CapabilityItemHandler`). |
| 15 | + |
| 16 | +Pour les utiliser, il faut d'abord les attacher à la `BlockEntity`/`Entity`/`ItemStack`/`Level`/`LevelChunk` de votre choix. |
| 17 | +### Attacher une Capability |
| 18 | + |
| 19 | +#### Récupérer l'instance d'une capability |
| 20 | + |
| 21 | +Pour attacher une capability, il faut déjà posséder son unique instance. Pour cela, vous pouvez l'obtenir soit dans la classe qui la contient par défaut, soit en obtenant une autre référence de la même instance en utilisant `CapabilityManager#get` comme ceci: |
| 22 | +```java |
| 23 | +static Capability<VotreInterface> VOTRE_CAPABILITY = CapabilityManager.get(new CapabilityToken<>(){}); |
| 24 | +``` |
| 25 | +Où VotreInterface est l'interface de votre capability et VOTRE_CAPABILITY est le nom que vous voulez donner à votre variable(appelez-la comme vous voulez) |
| 26 | + |
| 27 | +Exemple : |
| 28 | +```java |
| 29 | +static Capability<IEnergyStorage> ENERGY_STORAGE = CapabilityManager.get(new CapabilityToken<>(){}); |
| 30 | +``` |
| 31 | +#### Attacher une Capability |
| 32 | + |
| 33 | +Pour attacher une Capability, il faut passer par l'évènement `AttachCapabilitiesEvent`: |
| 34 | +* `AttachCapabilitiesEvent<Entity>` pour les `Entity` |
| 35 | +* `AttachCapabilitiesEvent<BlockEntity>` pour les `BlockEntity` |
| 36 | +* `AttachCapabilitiesEvent<ItemStack>` pour les `ItemStack` |
| 37 | +* `AttachCapabilitiesEvent<Level>` pour les `Level` |
| 38 | +* `AttachCapabilitiesEvent<LevelChunk>` pour les `LevelChunk` |
| 39 | + |
| 40 | +:::caution |
| 41 | +Il n'existe d'évènement que pour ces cinq-là. Par exemple, si vous voulez attacher une Capability à un joueur spécifiquement, `AttachCapabilitiesEvent<Player>` ne marchera pas. À la place, il faut utiliser `AttachCapabilitiesEvent<Entity>` et vérifier si `AttachCapabilitiesEvent#getObject`(l'entité) est une instance de Player. |
| 42 | +:::caution |
| 43 | +Vous devrez avoir une implémentation de votre capability(utilisez celle par défaut ou créez la vôtre, voir [ici](#les-implémentations-de-linterface-de-votre-capability)). |
| 44 | + |
| 45 | +Il vous faudra également une `ResourceLocation` qui sera la "clé" de votre capability et qui sera utilisée pour éviter que la même capability soit ajoutée deux fois ou que d'autres erreurs du style se produisent. |
| 46 | + |
| 47 | +:::tip |
| 48 | +Votre clé peut être n'importe quelle `ResourceLocation`, mais elle doit être unique. |
| 49 | + |
| 50 | +Vous pouvez, par exemple, créer une `ResourceLocation` à partir de votre modid et du nom de la Capability que vous ajoutez comme ceci : |
| 51 | + |
| 52 | +```java |
| 53 | +ResourceLocation VOTRE_CLE = new ResourceLocation(VOTRE_MODID, NOM_DE_LA_CAPABILITY) |
| 54 | +``` |
| 55 | +:::tip |
| 56 | + |
| 57 | +Pour finir, il vous faudra une implémentation de `ICapabilityProvider` qui retourne avec la fonction `getCapability` un `LazyOptional` de la capability(un Provider) |
| 58 | + |
| 59 | +Exemple : |
| 60 | + |
| 61 | +```java |
| 62 | +public class EnergyStorageProvider implements ICapabilityProvider { |
| 63 | + private final LazyOptional<IEnergyStorage> energyStorageOptional; |
| 64 | + |
| 65 | + public EnergyStorageProvider(){ |
| 66 | + this.energyStorageOptional = LazyOptional.of(() -> new EnergyStorage(10000)); // Remplacez le new EnergyStorage() par votre implémentation de l'interface de la capability |
| 67 | + } |
| 68 | + |
| 69 | + @Nonnull |
| 70 | + @Override |
| 71 | + public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) { |
| 72 | + return CapabilityEnergy.ENERGY.orEmpty(cap, this.energyStorageOptional); |
| 73 | + } |
| 74 | + |
| 75 | +} |
| 76 | +``` |
| 77 | +Pensez bien à remplacer `IEnergyStorage` par l'interface de votre capability, `new EnergyStorage()` par l'implémentation de votre capability et `CapabilityEnergy.ENERGY` par l'unique instance de la capability(regardez [ici](#récupérer-linstance-dune-capability) pour savoir comment l'obtenir) |
| 78 | + |
| 79 | +Exemples : |
| 80 | + |
| 81 | +Attacher la Capability `IEnergyStorage` avec le Provider fait plus haut à tous les `LevelChunk` : |
| 82 | +```java |
| 83 | +@SubscribeEvent |
| 84 | +public static void attachToChunks(AttachCapabilitiesEvent<LevelChunks> event) |
| 85 | +{ |
| 86 | + event.addCapability(VOTRE_CLE, new EnergyStorageProvider()); |
| 87 | +} |
| 88 | +``` |
| 89 | +Attacher la Capability `IEnergyStorage` avec le Provider fait plus haut à des `Player` : |
| 90 | +```java |
| 91 | +@SubscribeEvent |
| 92 | +public static void attachToEntities(AttachCapabilitiesEvent<Entity> event) |
| 93 | +{ |
| 94 | + if(event.getObject() instanceof Player){ |
| 95 | + event.addCapability(VOTRE_CLE, new EnergyStorageProvider()); |
| 96 | + } |
| 97 | +} |
| 98 | +``` |
| 99 | +(pensez bien à remplacer VOTRE_CLE par la `ResourceLocation` servant de clé que vous avez créée plus haut et `new EnergyStorageProvider()` par votre provider) |
| 100 | + |
| 101 | +:::warning |
| 102 | +Attacher les Capabilities par défaut de Forge(voir [ici](#utiliser-une-capability)) à des classes vanilla peut causer certains problèmes. Par exemple, attacher un `IItemHandler` à un joueur ne marchera pas, car si vous essayez de le récupérer en passant par le joueur, vous obtiendrez un `IItemHandler` qui correspond à l'inventaire du même joueur. Si vous souhaitez tout de même utiliser les Capabilities de Forge, il faut alors créer une nouvelle Capability qui extend celle que vous souhaitez attacher(voir [ici](#créer-une-capability)). |
| 103 | +::: |
| 104 | + |
| 105 | +### Récupérer la Capability |
| 106 | + |
| 107 | +Une fois que la Capability est bien attachée, pour l'utiliser, il faut la récupérer ! Pour cela, reprenons l'exemple de la Capability `IEnergyStorage` attachée à un joueur : |
| 108 | + |
| 109 | +Pour la récupérer, il faut d'abord obtenir la classe sur laquelle vous avez attaché la Capability (une instance de `Player` dans notre cas). Une fois cela fait (je n'explique pas comment faire, car cela dépend de sur quoi vous avez attaché la Capability), il faut utiliser dans notre cas(celui du joueur) : |
| 110 | + |
| 111 | +```java |
| 112 | +LazyOptional<IEnergyStorage> energyStorageLazyOptional = player.getCapability(CapabilityEnergy.ENERGY); |
| 113 | +``` |
| 114 | + |
| 115 | +Encore une fois, remplacez bien `IEnergyStorage` par l'interface de votre Capability, `CapabilityEnergy.ENERGY` par l'instance de votre Capability et nommez la variable comme vous voulez. |
| 116 | + |
| 117 | +Nous avons maintenant un `LazyOptional` de notre Capability. |
| 118 | +:::tip |
| 119 | +Qu'est-ce qu'un `LazyOptional` ? |
| 120 | + |
| 121 | +C'est une classe créée par Forge et qui est similaire à la classe Optional (tapez java Optional sur google si vous ne savez pas ce que c'est). Si vous voulez en savoir plus, regardez dans la classe elle-même, c'est assez bien documenté. |
| 122 | +::: |
| 123 | + |
| 124 | +Maintenant que vous possédez votre `LazyOptional`, vous pouvez faire ce que vous voulez avec. |
| 125 | + |
| 126 | +Vous pouvez utiliser `LazyOptional#isPresent` pour savoir si votre Capability est présente, `LazyOptional#ifPresent` pour exécuter un `Consumer` si la capability est présente, et d'autres comme `LazyOptional#orElse`. Pour plus d'informations, je vous invite à regarder dans la class `LazyOptional`, sur [cette page](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) ou à rechercher sur Internet. |
| 127 | + |
| 128 | +Exemples (si l'on utilise la Capability `IEnergyStorage`) : |
| 129 | +```java |
| 130 | +lazyOptional.ifPresent(cap -> cap.receiveEnergy(500, false)); |
| 131 | + |
| 132 | +IEnergyStorage energyStorage = lazyOptional.orElse(new EnergyStorage(10000)); |
| 133 | +energyStorage.extractEnergy(500, false); |
| 134 | +``` |
| 135 | + |
| 136 | +### Sauvegarder la Capability |
| 137 | + |
| 138 | +Si vous avez fait quelques tests par vous-mêmes, vous avez sûrement remarqué que la Capability n'est pas sauvegardée : c'est normal. |
| 139 | + |
| 140 | +Pour sauvegarder sa Capability, il faut modifier votre Provider comme ceci : |
| 141 | + |
| 142 | +- Tout d'abord, il faut savoir quel type de données vous voulez sauvegarder et trouver le `Tag`(anciennement `NBT`) correspondant : `IntTag` si vous souhaitez sauvegarder un `int`, `StringTag` pour un `String`, ou encore `CompoundTag` pour stocker différents types de données. Il en existe beaucoup d'autres, je vous invite donc à regarder le package *`net.minecraft.nbt`* pour la liste complète. |
| 143 | +- Ensuite, changez votre classe pour implémenter `ICapabilitySerializable<VotreTag>`(remplacez VotreTag par le `Tag` que vous avez choisi) au lieu de `ICapabilityProvider`. Cela devrait générer une erreur, c'est normal. |
| 144 | +- Ajoutez la fonction `serializeNBT` qui retourne le `Tag` que vous avez décidé d'utiliser que vous aurez préalablement set avec les données que vous voulez sauvegarder |
| 145 | +- Finalement, ajoutez la fonction `deserializeNBT` qui a pour argument le `Tag` que vous avez décidé d'utiliser et que vous pouvez récupérer pour l'utiliser |
| 146 | + |
| 147 | +:::tip |
| 148 | +La plupart des implémentations par défaut des Capabilities fournies par Forge(regardez les classes qui implémentent l'interface de la Capability de votre choix) possèdent des fonctions permettant de sérialiser et de désérialiser des `Tag`. Si elles existent, il est donc préférable de les utiliser dans les fonctions correspondantes de votre Provider. |
| 149 | +::: |
| 150 | + |
| 151 | +Voici ce que cela donne si l'on reprend le Provider créé plus haut : |
| 152 | +```java |
| 153 | +public class EnergyStorageProvider implements ICapabilitySerializable<IntTag> { |
| 154 | + private final LazyOptional<IEnergyStorage> energyStorageOptional; |
| 155 | + |
| 156 | + public EnergyStorageProvider(){ |
| 157 | + this.energyStorageOptional = LazyOptional.of(() -> new EnergyStorage(10000)); // Remplacez le new EnergyStorage() par votre implémentation de l'interface de la capability |
| 158 | + } |
| 159 | + |
| 160 | + @Nonnull |
| 161 | + @Override |
| 162 | + public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) { |
| 163 | + return CapabilityEnergy.ENERGY.orEmpty(cap, this.energyStorageOptional); |
| 164 | + } |
| 165 | + |
| 166 | + @Override |
| 167 | + public IntTag serializeNBT() { |
| 168 | + LazyOptional<EnergyStorage> energyStorage1 = energyStorageOptional.cast(); //Cette ligne sert à transformer le LazyOptional qui contient un IEnergyStorage en LazyOptional qui contient un EnergyStorage. Si vous faites ceci, soyez bien sûrs que votre LazyOptional du début contienne forcément une instance de la classe que vous castez, sinon vous aurez une erreur |
| 169 | + return (IntTag) energyStorage1.orElseThrow(() -> new IllegalArgumentException("Unable to serialize the capability : the capability is not present")).serializeNBT(); |
| 170 | + } |
| 171 | + |
| 172 | + @Override |
| 173 | + public void deserializeNBT(IntTag nbt) { |
| 174 | + LazyOptional<EnergyStorage> energyStorage1 = energyStorageOptional.cast(); |
| 175 | + energyStorage1.orElseThrow(() -> new IllegalArgumentException("Unable to deserialize the capability : the capability is not present")).deserializeNBT(nbt); |
| 176 | + } |
| 177 | + |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +### Créer une Capability |
| 182 | + |
| 183 | +Si aucune des Capabilities fournies par Forge ne vous convient, vous pouvez créer la vôtre. |
| 184 | + |
| 185 | +Pour ce faire, vous allez devoir créer plusieurs classes : l'interface de la Capability, une ou plusieurs implémentations et enfin une classe qui va contenir l'instance de la Capability. |
| 186 | + |
| 187 | +#### L'interface de la Capability |
| 188 | + |
| 189 | +Cette partie est relativement simple et dépend beaucoup de l'usage que vous voulez faire de votre Capability. Créez juste les fonctions dont vous avez besoin. |
| 190 | + |
| 191 | +Exemple : |
| 192 | +```java |
| 193 | +public interface ILightCapability { |
| 194 | + |
| 195 | + /** |
| 196 | + * Used to get the amount of light stored |
| 197 | + * @return the amount of light stored |
| 198 | + */ |
| 199 | + int getLight(); |
| 200 | + |
| 201 | + /** |
| 202 | + * Used to define the light stored |
| 203 | + * @param light the new amount of light |
| 204 | + */ |
| 205 | + void setLight(int light); |
| 206 | + |
| 207 | + /** |
| 208 | + * Used to add an amount of light to the storage |
| 209 | + * @param light the amount of light to be added to the storage |
| 210 | + */ |
| 211 | + default void receiveLight(int light){ |
| 212 | + setLight(getLight() + light); |
| 213 | + } |
| 214 | + |
| 215 | + /** |
| 216 | + * Used to remove an amount of light to the storage |
| 217 | + * @param light the amount of light to be removed from the storage |
| 218 | + */ |
| 219 | + default void extractLight(int light){ |
| 220 | + setLight(getLight() - light); |
| 221 | + } |
| 222 | + |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +#### Les implémentations de l'interface de votre Capability |
| 227 | + |
| 228 | +Après avoir créé l'interface de sa Capability, il faut aussi créer des implémentations de cette même interface : ce seront elles qui seront utilisées en temps réel par le biais des `LazyOptional` lorsque vous récupérez votre Capability. |
| 229 | + |
| 230 | +La seule "règle" est qu'il faut que vous implémentiez votre interface, et vous pouvez aussi faire comme Forge et rajouter des fonctions pour sérialiser et désérialiser les données contenues dans la classe, pour rendre le code plus facile à comprendre et à éditer si vous utilisez plusieurs Provider par exemple. |
| 231 | + |
| 232 | +Exemple : |
| 233 | +```java |
| 234 | +public class LightStorage implements ILightCapability{ |
| 235 | + private final int maxLight; |
| 236 | + private int light; |
| 237 | + |
| 238 | + public LightStorage(int maxLight){ |
| 239 | + this.maxLight = maxLight; |
| 240 | + } |
| 241 | + |
| 242 | + @Override |
| 243 | + public int getLight() { |
| 244 | + return light; |
| 245 | + } |
| 246 | + |
| 247 | + @Override |
| 248 | + public void setLight(int light) { |
| 249 | + light = Math.min(light, maxLight); //Un peu de code pour empêcher le niveau de lumière ("light") de dépasser la valeur maximale définie dans le constructeur ou d'être en dessous de 0 |
| 250 | + light = Math.max(light, 0); |
| 251 | + this.light = light; |
| 252 | + } |
| 253 | + |
| 254 | + public Tag serializeNBT(){ |
| 255 | + return IntTag.valueOf(getLight()); |
| 256 | + } |
| 257 | + |
| 258 | + public void deserializeNBT(Tag nbt){ |
| 259 | + if(nbt instanceof IntTag){ |
| 260 | + setLight(((IntTag) nbt).getAsInt()); |
| 261 | + } |
| 262 | + } |
| 263 | +} |
| 264 | +``` |
| 265 | + |
| 266 | +#### Créer la classe contenant l'instance de la Capability |
| 267 | + |
| 268 | +Il faut maintenant créer une classe qui contiendra l'instance par défaut de votre Capability(il s'agit en fait d'une instance de la classe `Capability`) |
| 269 | + |
| 270 | +Le code pour la récupérer est exactement le même qu'[ici](#récupérer-linstance-dune-capability), il faut juste mettre ça dans une classe. |
| 271 | + |
| 272 | +Exemple : |
| 273 | +```java |
| 274 | +public class LightCapability { |
| 275 | + public static Capability<ILightCapability> LIGHT_CAPABILITY = CapabilityManager.get(new CapabilityToken<>(){}); |
| 276 | +} |
| 277 | +``` |
| 278 | + |
| 279 | +#### Enregistrer la Capability |
| 280 | + |
| 281 | +Finalement, il faut enregistrer sa Capability à l'aide de la fonction `register` de l'event `RegisterCapabilitiesEvent` pour que Forge sache qu'elle existe. |
| 282 | + |
| 283 | +N'oubliez pas de faire attention à ce que la classe dans laquelle vous mettez l'event soit bien "abonnée" aux flux d'events ! |
| 284 | + |
| 285 | +Exemple : |
| 286 | +```java |
| 287 | +@SubscribeEvent |
| 288 | +public void registerCaps(RegisterCapabilitiesEvent event) { |
| 289 | + event.register(ILightCapability.class); |
| 290 | +} |
| 291 | +``` |
0 commit comments