Implémenter des DataManipulators

Ce guide est destiné aux contributeurs qui souhaitent aider à l’implémentation de la Data API en créant des DataManipulators. Une liste à jour de tous les DataManipulators à implémenter peut être trouvée sur la page SpongeCommon Issue #8 (GitHub).

Pour totalement implémenter un DataManipulator, les étapes suivantes doivent être respectées:

  1. Implémenter le DataManipulator en lui-même

  2. Implémenter le ImmutableDataManipulator

Après avoir suivi ces étapes, les suivantes doivent être faites:

  1. Enregistrer la Key dans le KeyRegistry

  2. Implémenter le DataProcessor

  3. Implémenter le ValueProcessor pour chaque valeur représentée par le DataManipulator

  4. Tout enregistrer dans le SpongeSerializationRegistry

Si les données s’appliquent à un bloc, plusieurs méthodes doivent également être mélangées au bloc.

Note

Assurez-vous de suivre nos Lignes directrices des contributions.

1. Implémenter le DataManipulator

La convention de nommage pour les implémentations d’un DataManipulator est le nom de l’interface préfixé avec « Sponge ». Donc pour implémenter l’interface HealthData, nous créerons une classe nommée SpongeHealthData dans le package approprié. Pour implémenter le DataManipulator, il faut tout d’abord étendre une classe abstraite appropriée du package org.spongepowered.common.data.manipulator.mutable.common. Le plus générique est l”AbstractData, mais il y a aussi des abstractions qui réduisent encore plus la taille du code pour certains cas particuliers comme les DataManipulators qui ne contiennent qu’une seule valeur.

public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {
    [...]
}

Il y a deux arguments à la classe AbstractData. Le premier est l’interface implémentée par la classe, le second est l’interface implémentée par le ImmutableDataManipulator correspondant.

Le Constructeur

Dans la plupart des cas, lorsque vous implémentez un DataManipulator abstrait, vous devez avoir deux constructeurs :

  • Un sans arguments (no-args) qui appelle le second constructeur avec des valeurs « par défaut »

  • L’autre constructeur qui prend toutes les valeurs qu’il supporte.

Le second constructeur doit

  • faire appel au constructeur AbstractData, en passant la référence de la classe pour l’interface implémentée.

  • s’assurer que les valeurs données sont valides

  • appeler la méthode registerGettersAndSetters()

import static com.google.common.base.Preconditions.checkArgument;

public class SpongeHealthData {

    public SpongeHealthData() {
        this(DataConstants.DEFAULT_HEALTH, DataConstants.DEFAULT_HEALTH);
    }

    public SpongeHealthData(double currentHealth, double maxHealth) {
        super(HealthData.class);
        checkArgument(currentHealth >= DataConstants.MINIMUM_HEALTH && currentHealth <= (double) Float.MAX_VALUE);
        checkArgument(maxHealth >= DataConstants.MINIMUM_HEALTH && maxHealth <= (double) Float.MAX_VALUE);
        this.currentHealth = currentHealth;
        this.maximumHealth = maxHealth;
        this.registerGettersAndSetters();
    }

    ...

}

Maintenant que nous savons que la vie actuelle et la vie maximum ont une limite, nous devons vérifier qu’aucune valeur donnée ne dépasse ces limites. Pour faire cela, nous utiliserons la Preconditions de guava dont nous importerons les méthodes requises statiquement.

Note

N’utilisez jamais ce que l’on appelle « valeurs magiques » (des nombres arbitraires, booléens, etc.) dans votre code. Au lieu de cela, localisez la classe org.spongepowered.common.data.util.DataConstants et utilisez une constante de raccord - ou en créer une, si nécessaire.

Accesseurs définis par l’Interface

L’interface que nous implémentons spécifie quelques méthodes pour accéder à l’objet Value. Pour HealrhData, ce sont health() et maxHealth(). Chaque appel qui leur est fait devrait donner une nouvelle Value.

public MutableBoundedValue<Double> health() {
    return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
        .minimum(DataConstants.MINIMUM_HEALTH)
        .maximum(this.maximumHealth)
        .defaultValue(this.maximumHealth)
        .actualValue(this.currentHealth)
        .build();
}

Astuce

Depuis que Double est un Comparable, nous n’avons plus besoin de spécifier un comparateur.

Si aucune valeur n’est actuellement spécifiée, l’appel get() sur la Value retourne la valeur par défaut.

Copie et Serialization

Les deux méthodes copy() et asImmutable() ne sont pas compliquées à implémenter. Pour les deux, vous n’aurez qu’à retourner un data manipulator mutable ou immutable respectivement, contenant les mêmes données que l’instance actuelle.

La méthode toContainer() est utilisée pour la sérialisation. Utilisez un MemoryDataContainer comme résultat et appliquez-le aux valeurs stockées dans cette instance. Un DataContainer est surtout un map qui mappe les DataQuerys aux valeurs. Puisqu’une Key contient toujours un DataQuery correspondant, il suffit d’utiliser ceux-ci en passant la Key directement.

public DataContainer toContainer() {
    return new MemoryDataContainer()
        .set(Keys.HEALTH, this.currentHealth)
        .set(Keys.MAX_HEALTH, this.maximumHealth);
}

registerGettersAndSetters()

Un DataManipulator permet également de fournir des méthodes qui servent à obtenir et définir des données en utilisant des clés. L’implémentation pour le faire est gérée par AbstractData, mais il faut lui dire à quelle donnée accéder et comment y accéder. Il nous faut donc suivre les instructions suivantes pour chaque valeur de la méthode registerGettersAndSetters():

  • enregistrer un Supplier pour directement obtenir la valeur

  • enregistrer un Consumer pour directement définir la valeur

  • enregistrer un Supplier<Value> pour obtenir la mutable Value

Le Supplier et le Consumer sont des interfaces fonctionnelles, les Lambdas de Java 8 peuvent donc être utilisées.

private void setCurrentHealthIfValid(double value) {
    if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
        this.currentHealth = value;
    } else {
        throw new IllegalArgumentException("Invalid value for current health");
    }
}

private void setMaximumHealthIfValid(double value) {
    if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
        this.maximumHealth = value;
    } else {
        throw new IllegalArgumentException("Invalid value for maximum health");
    }

}

private void registerGettersAndSetters() {
    registerFieldGetter(Keys.HEALTH, () -> SpongeHealthData.this.currentHealth);
    registerFieldSetter(Keys.HEALTH, SpongeHealthData.this::setCurrentHealthIfValid);
    registerKeyValue(Keys.HEALTH, SpongeHealthData.this::health);

    registerFieldGetter(Keys.MAX_HEALTH, () -> SpongeHealthData.this.maximumHealth);
    registerFieldSetter(Keys.MAX_HEALTH, SpongeHealthData.this::setMaximumHealthIfValid);
    registerKeyValue(Keys.MAX_HEALTH, SpongeHealthData.this::maxHealth);
}

Le Consumer enregistré en tant que setter de champ doit effectuer les vérifications adéquates pour s’assurer que la valeur fournie est valide. Cela vaut surtout pour les DataHolder``s qui n'acceptent pas les valeurs négatives. Si une valeur est invalide, une ``IllegalArgumentException doit être levée.

Astuce

Les critères de validité pour ces setters sont les mêmes que pour l’objet Value respectif, ainsi vous pouvez déléguer la vérification de la validité à un appel de this.health().set() et juste définir this.currentHealth = value si la première ligne n’a pas encore levé d’exception.

Et voilà. Le DataManipulator devrait être terminé maintenant.

2. Implémenter le ImmutableDataManipulator

Implémenter le ImmutableDataManipulator est similaire à l’implémentation de celui qui est mutable.

Les seules différences sont :

  • Le nom de la classe est formé en préfixant le nom des DataManipulators mutables avec ImmutableSponge

  • Il hérite de ImmutableAbstractData à la place

  • Au lieu de registerGettersAndSetters(), la méthode s’appelle registerGetters()

Lors de la création des ImmutableDataHolders ou des ImmutableValues, vérifions si il est judicieux d’utiliser le ImmutableDataCachingUtil. Par exemple, si vous avez une WetData qui ne contient rien d’autre qu’un booléen, il est plus facile de ne retenir que deux instances mises en cache de ImmutableWetData - une pour chaque valeur possible. Pour les manipulateurs et les valeurs avec plusieurs valeurs possibles (comme SignData), la mise en cache serait trop gourmande.

Astuce

Vous devez déclarer les champs d’un ImmutableDataManipulator comme final afin d’éviter tous changements accidentels.

3. Enregistrer la Key dans le KeyRegistry

La prochaine étape est d’enregistrer vos Keys au KeyRegistry. Pour ce faire, localisez la classe org.spongepowered.common.data.key.KeyRegistry et trouvez la fonction static generateKeyMap(). Ici ajoutez une ligne pour enregistrer (et créer) vos clés utilisées.

keyMap.put("health"), makeSingleKey(Double.class, MutableBoundedValue.class, of("Health")));
keyMap.put("max_health", makeSingleKey(Double.class, MutableBoundedValue.class, of("MaxHealth")));

La keyMap mappe les chaînes de caractères en Keys. La chaîne utilisée doit être le nom de la constante correspondant de la classe utilitaire Keys` en minuscules. La Key``elle-même est créée par une des méthodes statiques fournies par le ``KeyFactory, dans la plupart des cas makeSingleKey. makeSingleKey requiert d’abord la référence d’une classe pour les doonnées sous-jacentes, qui, dans notre cas est un « Double », puis une référence de classe pour le type de Value utilisé. Le troisième argument est la DataQuery``utilisée pour la sérialisation. Elle est créée à partir de la méthode importée statiquement ``DataQuery.of(), qui accepte une chaîne de caractères. Cette chaîne doit également être le nom de la constante, sans les tirets-bas (underscores) et avec la capitalisation changée en upper camel case.

4. Implémenter les DataProcessors

Ensuite vient le DataProcessor. Un DataProcessor sert de pont entre notre DataManipulator et les objets de Minecraft. Chaque fois que des données sont demandées ou offertes aux DataHolders qui existent dans Minecraft de base, ces appels finissent par être délégués à un DataProcessor ou un ValueProcessor.

Pour votre nom, vous devez utiliser le nom de l’interface DataManipulator et ajouter Processor. Ainsi, pour HealthData nous créons un HealthDataProcessor.

Afin de réduire le code réutilisable, le DataProcessor doit hériter de la classe abstraite appropriée dans le package org.spongepowered.common.data.processor.common. Puisque la santé peut seulement être présente sur certaines entités, nous pouvons faire usage du AbstractEntityDataProcessor qui vise spécifiquement les Entities issues de net.minecraft.entity.Entity. AbstractEntitySingleDataProcessor nécessiterait moins de travail d’implémentation, mais ne peut pas être utilisé puisque HealthData contient plus d’une valeur.

public class HealthDataProcessor extends AbstractEntityDataProcessor<EntityLivingBase, HealthData, ImmutableHealthData> {
    public HealthDataProcessor() {
        super(EntityLivingBase.class);
    }
    [...]
}

Selon quel abstraction vous utilisez, les méthodes que vous devez implémenter peuvent différer grandement, selon la quantité de travail d’implémentation qui pourrait déjà être fait dans la classe abstraite. Généralement, les méthodes peuvent être classées.

Astuce

Il est possible de créer plusieurs DataProcessors pour les mêmes données. Si des DataHolders largement différents doivent être supportés (par exemple une TileEntity et un ItemStack correspondant), il peut être avantageux de créer un processeur pour chaque type de DataHolder afin d’utiliser pleinement les abstractions fournies. Veillez à suivre la structure de packages pour les items, les tileentities et les entities.

Méthodes de Validation

Retournez toujours une valeur booléenne. Si la méthode est appelée supports() elle devra effectuer une vérification générale de si la cible fournie prend généralement en charge le type de données gérées par notre DataProcessor.

Pour notre HealthDataProcessor, supports() est implémenté par le AbstractEntityDataProcessor. Par défaut, il va retourner true si l’argument fourni est une instance de la classe spécifiée lors de l’appel du constructeur super().

Au lieu de cela, nous sommes obligés de fournir une méthode doesDataExist(). Puisque l’abstraction ne sait pas comment obtenir les données, il laisse cette fonction à être implémentée. Comme le nom le dit, la méthode doit vérifier si les données existent déjà sur la cible supportée. Pour le HealthDataProcessor, cela retourne toujours true, puisque chaque entité vivante possède toujours de la santé.

protected boolean doesDataExist(EntityLivingBase entity) {
    return true;
}

Méthodes Setter

Une méthode setter reçoit un DataHolder d’une sorte et certaines données qui devraient lui être appliquées, si possible.

L’interface DataProcessor définit une méthode set() acceptant un DataHolder et un DataManipulator, et qui retourne un DataTransactionResult. Selon la classe d’abstraction utilisée, certaines des fonctionnalités nécessaires peuvent déjà être implémentées.

Dans ce cas, l”AbstractEntityDataProcessor s’occupe de la plupart d’elles et nécessite juste une méthode pour définir certaines valeurs poru retourner true si c’était réussi et false si ça ne l’était pas. Toutes les vérifications de si le DataHolder supporte la Data sont prises en charge, la classe abstraite va juste passer un mappage de Map pour chaque Key depuis le DataManipulateur à sa valeur, et construit alors un DataTransactionResult selon si l’opération a réussi ou non.

protected boolean set(EntityLivingBase entity, Map<Key<?>, Object> keyValues) {
    entity.getEntityAttribute(SharedMonsterAttributes.maxHealth)
        .setBaseValue(((Double) keyValues.get(Keys.MAX_HEALTH)).floatValue());
    entity.setHealth(((Double) keyValues.get(Keys.HEALTH)).floatValue());
    return true;
}

Astuce

Pour comprendre les DataTransactionResults, vérifiez la page de docs correspondante et référez-vous aux docs DataTransactionResult.Builder pour en créer un.

Avertissement

Surtout lors du travail avec les ItemStacks, il est probable que vous aurez besoin de faire face directement à des NBTTagCompounds. De nombreuses clés NBT sont déjà définies comme constantes dans la classe org.spongepowered.common.data.util.NbtDataUtil. Si votre clé requise n’est pas là, vous devez l’ajouter afin d’éviterr les “nombres magiques” dans le code.

Méthode de Suppression

La méthode remove() tente de supprimer les données du DataHolder et retourne un DataTransactionResult.

La suppression n’est pas abstraite dans n’importe quel DataProcessor abstrait puisque les abstractions n’ont aucun moyen de savoir si les données sont toujours présentes sur un DataHolder compatible (comme WetData ou HealthData) ou si elles peuvent ou ne peuvent pas être présentes (comme LoreData). Si les données sont toujours présentes, remove() doit toujours échouer. Si elles peuvent ou peuvent ne pas être présentes, remove() doit les supprimer. Dans ces cas, la méthode doesDataExist() doit être réécrite.

Puisqu’une entité vivante possède toujours de la vie, HealthData est toujours présent et la suppression n’est par conséquent pas supportée. Donc nous retournons juste failNoData() et nous ne réécrivons pas la méthode doesDataExist().

public DataTransactionResult remove(DataHolder dataHolder) {
    return DataTransactionBuilder.failNoData();
}

Méthodes Getter

Les méthodes getter obtiennent des données depuis un DataHolder et retournent un DataManipulator optionnel. L’interface DataProcessor spécifie les méthodes from() et createFrom(), la différence est que from() va retourner Optional.empty() si le data holder est dompatible, mais ne contient pas les données, alors que createFrom() va fournir un DataManipulator détenant les valeurs par défaut dans ce cas.

Encore une fois, AbstractEntityDataProcessor va fournir la plupart des implémentations pour cela et ne nécessite qu’une méthode pour récupérer les valeurs présentes sur le DataHolder. Cette méthode est seulement appeler après que supports() et doesDataExist() aient retourné true, ce qui signifie qu’elle est exécutée sous l’hypothèse que les données sont présentes.

Avertissement

Si les données peuvent ne pas toujours exister sur le DataHolder cible, par exemple si la fonction remove() peut être réussie (voir ci-dessus), il est impératif de réécrire la méthode doesDataExist() afin qu’elle retourne true si les données sont présentes et false si elles ne le sont pas.

protected Map<Key<?>, ?> getValues(EntityLivingBase entity) {
    final double health = entity.getHealth();
    final double maxHealth = entity.getMaxHealth();
    return ImmutableMap.<Key<?>, Object>of(Keys.HEALTH, health, Keys.MAX_HEALTH, maxHealth);
}

Méthodes Filler

Une méthode filler est différente d’une méthode getter puisqu’elle reçoit un DataManipulator pour le remplir avec des valeurs. Ces valeurs viennent soit d’un DataHolder ou doivent être désérialisées à partir d’un DataContainer. La méthode retourne Optional.empty() si le DataHolder n’est pas compatible.

AbstractEntityDataProcessor traite déjà le remplissage depuis les DataHolders en créant un DataManipulator depuis le holder et le fusionne avec le manipulateur fourni, mais il ne peut pas fournir la désérialisation du DataContainer.

public Optional<HealthData> fill(DataContainer container, HealthData healthData) {
    final Optional<Double> health = container.getDouble(Keys.HEALTH.getQuery());
    final Optional<Double> maxHealth = container.getDouble(Keys.MAX_HEALTH.getQuery());
    if (health.isPresent() && maxHealth.isPresent()) {
        healthData.set(Keys.HEALTH, health.get());
        healthData.set(Keys.MAX_HEALTH, maxHealth.get());
        return Optional.of(healthData);
    }
    return Optional.empty();
}

La méthode fill() consiste à retourner un Optional des healthData altérées, si et seulement si toutes les données nécessaires peuvent être obtenus depuis le DataContainer.

Autres Méthodes

Selon la superclasse abstraite utilisée, certaines autres méthodes peuvent être nécessaires. Par exemple, AbstractEntityDataProcessor a besoin de créer des instances de DataManipulator en différents points. Il ne peut pas faire ça puisqu’il ne connaît ni la classe d’implémentation, ni le constructeur à utiliser. Par conséquent, il utilise une fonction abstraite qui doit être fournie par l’implémentation finale. Cela ne fait rien de plus que de créer un DataManipulator avec les données par défaut.

Si vous avez implémenté votre DataManipulator comme c’est recommandé, vous pouvez simplement utiliser le constructeur sans argument.

protected HealthData createManipulator() {
    return new SpongeHealthData();
}

5. Implémenter les ValueProcessors

Non seulement un DataManipulator peut être offert à un DataHolder, mais aussi une clé Value seule. Par conséquent, vous devez fournir au moins un ValueProcessor pour chaque Key présente dans votre DataManipulator. Un ValueProcessor est nommé d’après le nom de la constante de sa Key dans la classe Keys de manière similaire à son DataQuery. Le nom de la constante est dépouillée de tirets-bas (underscores), utilisée en upper camel case et puis suffixé par ValueProcessor.

Un ValueProcessor doit toujours hériter de AbstractSpongeValueProcessor, qui gère déjà une partie des vérifications supports() basées sur le type de DataHolder. Pour Keys.HEALTH, nous allons créer et construire HealthValueProcessor comme suit.

public class HealthValueProcessor extends AbstractSpongeValueProcessor<EntityLivingBase, Double,
    MutableBoundedValue<Double> {

    public HealthValueProcessor() {
        super(EntityLivingBase.class, Keys.HEALTH);
    }

    [...]
}

Maintenant le AbstractSpongeValueProcessor va nous soulager de la nécessité de vérifier si la valeur est supportée. Elle est supposée être supportée si le ValueContainer cible est de type EntityLivingBase.

Astuce

Pour un contrôle plus précis sur quels objets de EntityLivingBase sont supportés, la méthode supports(EntityLivingBase) peut être réécrite.

Encore une fois, la plupart du travail est fait par la classe d’abstraction. Nous devons juste implémenter deux méthodes auxiliaires pour créer une Value et son homologue immuable, et trois méthodes pour récupérer, définir et supprimer les données.

protected MutableBoundedValue<Double> constructValue(Double value) {
    return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
        .minimum(DataConstants.MINIMUM_HEALTH)
        .maximum((double) Float.MAX_VALUE)
        .defaultValue(DataConstants.DEFAULT_HEALTH)
        .actualValue(value)
        .build();
}

protected ImmutableValue<Double> constructImmutableValue(Double value) {
    return constructValue(value).asImmutable();
}
protected Optional<Double> getVal(EntityLivingBase container) {
    return Optional.of((double) container.getHealth());
}

Puisqu’il est impossible pour une EntityLivingBase de ne pas avoir de santé, cette méthode ne retournera jamais Optional.empty().

protected boolean set(EntityLivingBase container, Double value) {
    if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
        container.setHealth(value.floatValue());
        return true;
    }
    return false;
}

La méthode set() va retourner une valeur booléenne indiquant si la valeur a pu être définie correctement. Cette implémentation va rejeter les valeurs en dehors des limites utilisées dans nos méthodes de construction de valeur ci-dessus.

public DataTransactionResult removeFrom(ValueContainer<?> container) {
    return DataTransactionBuilder.failNoData();
}

Puisque les données sont garanties d’être toujours présentes, les tentatives pour les supprimer échoueront.

6. Enregistrer des Processeurs

Afin que Sponge soit capable d’utiliser nos manipulateurs et nos processeurs, nous devons les enregistrer. Cela se fait dans la classe org.spongepowered.common.data.SpongeSerializationRegistry. Dans la méthode setupSerialization il y a deux grands blocs d’enregistrements auxquels nous ajoutons nos processeurs.

DataProcessors

Un DataProcessor est enregistré parallèlement à l’interface et aux classes d’implémentation du DataManipulator qu’il gère. Pour chaque pair de DataManipulators mutables/immuables, au moins un DataProcessor doit être enregistré.

dataRegistry.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
    ImmutableHealthData.class, ImmutableSpongeHealthData.class,
    new HealthDataProcessor());

ValueProcessors

Les processeurs de valeurs sont enregistrés en bas de la même fonction. Pour chaque Key plusieurs processeurs peuvent être enregistrés par les appels suivants de la méthode registerValueProcessor().

dataRegistry.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
dataRegistry.registerValueProcessor(Keys.MAX_HEALTH, new MaxHealthValueProcessor());

Implémenter les Données de Blocs

Les données de blocs sont quelque peu différentes des autres types de données car elles sont implémentées par le mélange dans le bloc lui-même. Il existe plusieurs méthodes dans org.spongepowered.mixin.core.block.MixinBlock qui doivent être réécrites pour implémenter des données pour les blocs.

@Mixin(BlockHorizontal.class)
public abstract class MixinBlockHorizontal extends MixinBlock {

    [...]
}

supports() doit retourner true` si l'interface ``ImmutableDataManipulator est assignable depuis la Class passée en argument, ou si la superclasse le supporte.

@Override
public boolean supports(Class<? extends ImmutableDataManipulator<?, ?>> immutable) {
    return super.supports(immutable) || ImmutableDirectionalData.class.isAssignableFrom(immutable);
}

getStateWithData() doit retourner un nouveau BlockState avec les données du ImmutableDataManipulator appliquées. Si le manipulateur n’est pas directement supporté, la méthode doit déléguer à la superclasse.

@Override
public Optional<BlockState> getStateWithData(IBlockState blockState, ImmutableDataManipulator<?, ?> manipulator) {
    if (manipulator instanceof ImmutableDirectionalData) {
        final Direction direction = ((ImmutableDirectionalData) manipulator).direction().get();
        final EnumFacing facing = DirectionResolver.getFor(direction);
        return Optional.of((BlockState) blockState.withProperty(BlockHorizontal.FACING, facing));
    }
    return super.getStateWithData(blockState, manipulator);
}

getStateWithValue() est l’équivalent de getStateWithData(), mais fonctionne avec des Keys seules.

@Override
public <E> Optional<BlockState> getStateWithValue(IBlockState blockState, Key<? extends BaseValue<E>> key, E value) {
    if (key.equals(Keys.DIRECTION)) {
        final Direction direction = (Direction) value;
        final EnumFacing facing = DirectionResolver.getFor(direction);
        return Optional.of((BlockState) blockState.withProperty(BlockHorizontal.FACING, facing));
    }
    return super.getStateWithValue(blockState, key, value);
}

Enfin, getManipulators() doit retourner une liste de tous les ImmutableDataManipulators supportés par le bloc, ainsi que les valeurs actuelles pour le IBlockState fourni. Il doit inclure tous les ImmutableDataManipulators de la superclasse.

@Override
public List<ImmutableDataManipulator<?, ?>> getManipulators(IBlockState blockState) {
    return ImmutableList.<ImmutableDataManipulator<?, ?>>builder()
            .addAll(super.getManipulators(blockState))
            .add(new ImmutableSpongeDirectionalData(DirectionResolver.getFor(blockState.getValue(BlockHorizontal.FACING))))
            .build();
}

Informations Supplémentaires

Avec Data étant un concept assez abstrait dans Sponge, il est difficile de donner des directives générales sur la façon d’acquérir les données nécessaires depuis les classes de Minecraft. Il peut être utile de jeter un oeil aux processeurs déjà implémentés similaires à ceux sur lesquels vous travaillez pour obtenir une meilleure compréhension de la façon dont il devrait fonctionner.

Si vous êtes bloqué ou que vous hésitez sur certains points, aller sur le channel IRC #spongedev, les forums, ou ouvrez un ticket sur GitHub. Vérifiez la Data Processor Implementation Checklist pour connaître les conditions de contribution.