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:
Implémenter le
DataManipulator
en lui-mêmeImplémenter le
ImmutableDataManipulator
Après avoir suivi ces étapes, les suivantes doivent être faites:
Enregistrer la
Key
dans leKeyRegistry
Implémenter le
DataProcessor
Implémenter le
ValueProcessor
pour chaque valeur représentée par leDataManipulator
Tout enregistrer dans le
SpongeSerializationRegistry
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 DataManipulator
s 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 abstract Manipulator, vous avez 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 DataQuery
s 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 valeurenregistrer un
Consumer
pour directement définir la valeurenregistrer un
Supplier<Value>
pour obtenir la mutableValue
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
DataManipulator
s mutables avecImmutableSponge
Il hérite de
ImmutableAbstractData
à la placeAu lieu de
registerGettersAndSetters()
, la méthode s’appelleregisterGetters()
Lors de la création des ImmutableDataHolder
s ou des ImmutableValue
s, 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 Key
s 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 Key
s. 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 DataProcessor
s pour les mêmes données. Si des DataHolder
s 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 DataTransactionResult
s, 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 ItemStack
s, il est probable que vous aurez besoin de faire face directement à des NBTTagCompound
s. 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 DataManipulator
s 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());
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.