Implémenter des DataManipulators

Avertissement

These docs were written for SpongeAPI 7 and are likely out of date. If you feel like you can help update them, please submit a PR!

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 KeyRegistryModule

  2. Implémenter le DataProcessor

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

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.

Les extraits suivants montrent les imports/chemins de certaines classes de SpongeCommon dont vous aurez besoin:

import org.spongepowered.common.data.DataProcessor;
import org.spongepowered.common.data.ValueProcessor;
import org.spongepowered.common.data.manipulator.immutable.entity.ImmutableSpongeHealthData;
import org.spongepowered.common.data.manipulator.mutable.common.AbstractData;
import org.spongepowered.common.data.manipulator.mutable.entity.SpongeHealthData;
import org.spongepowered.common.data.processor.common.AbstractEntityDataProcessor;
import org.spongepowered.common.util.Constants;
import org.spongepowered.common.data.util.NbtDataUtil;
import org.spongepowered.common.registry.type.data.KeyRegistryModule;

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 extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {

    private double health;
    private double maxHealth;

    public SpongeHealthData() {
        this(20D, 20D);
    }

    public SpongeHealthData(double health, double maxHealth) {
        super(HealthData.class);
        checkArgument(maxHealth > 0);
        this.health = health;
        this.maxHealth = maxHealth;
        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 les 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 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 HealthData#health() et HealthData#maxHealth(). Chaque appel qui leur est fait devra donner une nouvelle Value.

public MutableBoundedValue<Double> health() {
    return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
        .minimum(0)
        .maximum(this.maxHealth)
        .defaultValue(this.maxHealth)
        .actualValue(this.health)
        .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 BaseValue#get() sur la Value retourne la valeur par défaut.

Copie et Serialization

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

La méthode DataSerializable#toContainer() est utilisée pour la sérialisation. Utilisez DataContainer#createNew() comme résultat et appliquez-y les valeurs stockées dans l’instance actuelle. Un DataContainer est surtout une map qui associe des 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 super.toContainer()
        .set(Keys.HEALTH, this.health)
        .set(Keys.MAX_HEALTH, this.maxHealth);
}

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 obtenir 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 SpongeHealthData setCurrentHealthIfValid(double value) {
    if (value >= 0 && value <= (double) Float.MAX_VALUE) {
        this.health = value;
    } else {
        throw new IllegalArgumentException("Invalid value for current health");
    }
    return this;
}

private SpongeHealthData setMaximumHealthIfValid(double value) {
    if (value >= 0 && value <= (double) Float.MAX_VALUE) {
        this.maxHealth = value;
    } else {
        throw new IllegalArgumentException("Invalid value for maximum health");
    }
    return this;
}

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

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

Le Consumer enregistré en tant que setter doit effectuer les vérifications adéquates pour s’assurer que la valeur fournie est valide. En particulier pour les DataHolders 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.health = 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()

Quand vous créez des ImmutableDataHolders ou des ImmutableValues, vérifiez s’il est judicieux d’utiliser le ImmutableDataCachingUtil. Par exemple, si vous avez un 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 KeyRegistryModule

Le prochaine étape est d’enregistrer votre Keys aux Keys. Pour ce faire, trouvez la classe KeyRegistryModule puis la méthode registerDefaults(). Vous y ajouterez une ligne pour enregistrer (et créer) vos clés.

import static org.spongepowered.api.data.DataQuery.of;

this.register("health", Key.builder()
        .type(TypeTokens.BOUNDED_DOUBLE_VALUE_TOKEN)
        .id("health")
        .name("Health")
        .query(of("Health"))
        .build());
this.register("max_health", Key.builder()
        .type(TypeTokens.BOUNDED_DOUBLE_VALUE_TOKEN)
        .id("max_health")
        .name("Max Health")
        .query(of("MaxHealth"))
        .build());

The register(Key) method registers your Keys for later use. The string used for the id should be the corresponding constant name from the Keys utility class in lowercase. The Key itself is created by using the Key.Builder provided by the Key#builder() method. You have to set a TypeToken, an id, human readable name and a DataQuery. The DataQuery is used for serialization. It is created from the statically imported DataQuery.of() method accepting a string. This string should also be the constant name, stripped of underscores and capitalization changed to 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

Retourne toujours une valeur booléenne. Si n’importe quelle des méthodes supports(target) est appelée ça doit effectuer une vérification générale si la cible fournie supporte généralement le type de données gérées par notre DataProcessor. Selon votre niveau d’abstraction vous pouvez ne pas avoir à l’implémenter du tout, si vous devez, implémentez juste le plus spécifique, comme les plus génériques leur délègue souvent.

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é.

@Override
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.

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

Astuce

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

Avertissement

Surtout lors du travail avec les ItemStacks il est probable que vous aillez besoin de faire face directement à des NBTTagCompounds. De nombreuses clés NBT sont déjà définies en tant que constantes dans la classe NbtDataUtil. Si votre clé requise n’est pas là, vous devez l’ajouter afin d’éviter des “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.

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 DataTransactionResult#failNoData().

@Override
public DataTransactionResult remove(DataHolder dataHolder) {
    return DataTransactionResult.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.

@Override
protected Map<Key<?>, ?> getValues(EntityLivingBase entity) {
    final double health = entity.getHealth();
    final double maxHealth = entity.getMaxHealth();
    return ImmutableMap.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.

@Override
public Optional<HealthData> fill(DataContainer container, HealthData healthData) {
    if (!container.contains(Keys.MAX_HEALTH.getQuery()) || !container.contains(Keys.HEALTH.getQuery())) {
        return Optional.empty();
    }
    healthData.set(Keys.MAX_HEALTH, getData(container, Keys.MAX_HEALTH));
    healthData.set(Keys.HEALTH, getData(container, Keys.HEALTH));
    return Optional.of(healthData);
}

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 obtenues 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.

@Override
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.

@Override
protected MutableBoundedValue<Double> constructValue(Double health) {
    return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
        .minimum(0D)
        .maximum(((Float) Float.MAX_VALUE).doubleValue())
        .defaultValue(20D)
        .actualValue(health)
        .build();
}

@Override
protected ImmutableBoundedValue<Double> constructImmutableValue(Double value) {
    return constructValue(value).asImmutable();
}
@Override
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().

@Override
protected boolean set(EntityLivingBase container, Double value) {
    if (value >= 0 && 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.

@Override
public DataTransactionResult removeFrom(ValueContainer<?> container) {
    return DataTransactionResult.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 DataRegistrar. 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é.

DataUtil.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().

DataUtil.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
DataUtil.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, allez sur le channel IRC #spongedev, le channel #dev sur Discord, les forums, ou ouvrez un ticket sur GitHub. Vérifiez la Data Processor Implementation Checklist pour connaître les conditions de contribution.