DataManipulators Customisés
La partie principale des données customisés est le DataManipulator. Pour l’implémenter, vous devez d’abord décider de si vous voulez créer une API séparée pour vos données personnalisées. De manière générale, il est préférable de séparer l’API de l’implémentation (comme SpongeAPI le fait), mais il ne sera pas visible par les autres développeurs donc vous pouvez simplement mettre les deux dans la même classe.
Vous voudrez définir une méthode d’API pour chaque « unité » de vos données, comme les String
, int
, ItemStack ou un type personnalisé comme Home
. Ces unités seront encapsulées dans une Value, ce qui leur permettront d’être accédés avec des Keys. Il existe diverses extensions de Value
selon l’objet représenté, tel que MapValue qui fournit les opérations standards de maps, ou MutableBoundedValue qui peut définir des limites pour des valeurs (comme les entiers). Les limites des valeurs sont vérifiées en utilisant un Comparator.
Maintenant, choisissez quel type d”AbstractData vous allez hériter. Alors que vous pourriez l’implémenter à partide zéro, ces types abstraits enlèvent beaucoup de travail qui est nécessaire pour faire l’implémentation des méthodes requises. Une liste complète peut être trouvé dans org.spongepowered.api.data.manipulator.mutable.common. Voir Types Uniques ou Types Composés ci-dessous pour les détailes d’implémentation de chaque type.
Vous devrez créer deux classes différentes - une qui est mutable et qui implémente DataManipulator et votre type abstrait, et une version immuable qui implémente ImmutableDataManipulator et votre type abstrait immuable.
Note
Toutes les données doivent avoir des versions mutables et immuables, vous devez implémenter les deux.
Pour tous les types, vous devrez définir les méthodes DataManipulator#asImmutable() / asMutable() - c’est aussi simple que de copier les objets existants dans un constructeur pour la version alternative.
Valeurs
Votre/vos getter(s) ont besoin de retourner une valeur. Dans l’exemple ci-dessous, nous récupérer la ValueFactory. Cela nous permet d’économiser beaucoup de type en utiliser les objets Value
déjà implémentés par Sponge. Selon la valeur que vous créez il y a différentes méthodes à appeler comme createMapValue
, createBoundedComparableValue
, etc.
Exemple de Code : Implémenter un getter de valeur
import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.value.ValueFactory;
import org.spongepowered.api.data.value.mutable.Value;
import org.spongepowered.cookbook.myhomes.data.home.Home;
import org.spongepowered.cookbook.myhomes.data.Keys;
@Override
protected Value<Home> defaultHome() {
return Sponge.getRegistry().getValueFactory()
.createValue(Keys.DEFAULT_HOME, getValue(), null);
}
Notez qu’un ImmutableDataManipulator
retourne une ImmutableValue
, en appelant asImmutable()
sur la Value
retournée. Nous recommandons que vous mettez en cache cela (comme avec une variable de classe) dans la version immuable.
Chaque Value
a aussi besoin d’une Key pour l’identifier, comme vu dans l’exemple avec Keys.DEFAULT_HOME
. Similairement aux valeurs, vous utilisez une des méthodes makeXKey()
dans le KeyFactory pour créer une Key
pour votre valeur.
Vous avez besoin de passer un TypeToken
représentant le type brut de votre valeur, et un TypeToken
représentant la Value
. Vous aurez également besoin de fournir un chemin de DataQuery - c’est le plus souvent utilisé pour sérialisé la Value
. Comme avec n’importe quel type de catalogue, vous devez aussi fournir un ID unique et un nom. Mettez tout cela ensemble et vous avez une Key
que vous pouvez utiliser dans vos Value
s.
Exemple de Code : Créer une Key
import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.key.Key;
import org.spongepowered.api.data.key.KeyFactory;
import org.spongepowered.api.data.value.mutable.Value;
import org.spongepowered.api.data.value.mutable.Value;
import com.google.common.reflect.TypeToken;
import org.spongepowered.cookbook.myhomes.data.home.Home;
public static final Key<Value<Home>> DEFAULT_HOME = KeyFactory.makeSingleKey(
TypeToken.of(Home.class),
new TypeToken<Value<Home>>() {},
DataQuery.of("DefaultHome"), "myhomes:default_home", "Default Home");
Note
Les TypeTokens sont utilisés par l’implémentation pour préserver les types génériques de vos valeurs. Sponge fournit une longue liste de tokens pré-faits pour l’API dans TypeTokens.
Si vous avez besoin de créer le vôtre, vous pouvez le faire d’une ou deux manières :
Pour les types non génériques, utiliez
TypeToken.of(MyType.class)
Pour les types génériques, créez une classe anonyme avec
TypeToken<MyGenericType<String>>() {}
Sérialisation
Pour rendre vos données sérialisables pour les DataHolders ou les fichiers de configuration, vous devez aussi implémenter DataSerializable#toContainer(). Nous recommandons d’appeler super.toContainer()
puisqu’il inclue la version depuis le DataSerializable#getContentVersion(). Vous devez augmenter la version à chaque fois qu’un changement est fait au format de vos données sérialisées, et utiliser les Les DataContentUpdaters pour permettre la rétrocompatibilité.
Note
Ce n’est pas requis pour les types uniques simples, comme il implémente déjà toContainer()
Exemple de Code : Implémenter toContainer
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.cookbook.myhomes.data.Keys;
@Override
public DataContainer toContainer() {
DataContainer container = super.toContainer();
// This is the simplest, but use whatever structure you want!
container.set(Keys.DEFAULT_HOME.getQuery(), this.defaultHome);
container.set(Keys.HOMES, this.homes);
return container;
}
Enregistrement
Enregistrer votre DataManipulator
permet de le rendre accessible à Sponge et aux autres plugins d’un manière générique. Le jeu/plugin peut créer des copies de votre donnée et sérialiser/désérialiser votre donnée sans référencer aucune de vos classes directement.
Pour enregistrer un DataManipulator
Sponge a le DataRegistration#builder(). Il construira une DataRegistration et l’enregistrera automatiquement.
Note
En raison de la nature de la Data, vous devez enregistrer vos DataManipulator
durant l’initialisation - généralement en écoutant le GameInitializationEvent comme dans l’exemple ci-dessous. Si vous tentez d’enregistrer un DataManipulator
après l’initialisation, une exception sera lancée.
import org.spongepowered.api.event.game.state.GameInitializationEvent;
import org.spongepowered.api.data.DataRegistration;
import org.example.MyCustomData;
import org.example.ImmutableCustomData;
import org.example.CustomDataBuilder;
@Listener
public void onInit(GameInitializationEvent event) {
DataRegistration.builder()
.dataClass(MyCustomData.class)
.immutableClass(ImmutableCustomData.class)
.builder(new CustomDataBuilder())
.manipulatorId("my-custom")
.dataName("My Custom")
.build();
}
Avertissement
Les Datas sérialisées après la 6.0.0
ou celles dont vous avez changé l’ID ne seront pas reconnues à moins que vous les enregistriez avec le DataManager#registerLegacyManipulatorIds(String, DataRegistration). Si vous enregistrez un DataManipulator
pré-6.0.0, l’ID sera la valeur de Class.getName(), comme com.example.MyCustomData
.
Types Uniques
Les types uniques nécessitent peu d’implémentation puisqu’une grande partie du travail a déjà été faite dans le type AbstractSingleData que vous héritez.
Les types abstraits « simples » sont les plus faciles à implémenter, mais sont limités aux types ci-dessous :
Boolean
Integer
List
Map
CatalogType
Enum
Pour tous les autres types vous devez implémenter un type unique customisé en héritant AbstractSingleData
. Cela vous permet de définir vos propres données uniques avec les types que vous souhaitez, tout en faisant toujours l’essentials du travail pour vous.
Astuce
Les implémentations abstraites enregistrent l’objet pour vous dans le constructeur. Vous pouvez y accéder dans votre implémentation en appelant les méthodes getValue()
et getValueGetter()
.
Types Uniques Simples
Presque tout le travail est fait pour vous avec les types abstraits simples. Tout ce que vous devez faire est :
Héritez le type abstrait pertinent
passez la
Key
pour vos données, l’objet lui-même, et l’objet par défaut (si l’objet est null) dans le constructeur
AbstractBoundedComparableData (et l’équivalent immuable) exige également des valeurs minimum et maximum qui seront vérifies, ainsi qu’un Comparator.
Note
Les types uniques List
et Mapped
doivent plutôt implémenter ListData
/ MappedData
(ou l’équivalent immuable). Cela rajoute des méthodes additionnelles pour permettre des comportements similaires à ceux des Maps/Lists directement sur le DataManipulator
.
Les 3 méthodes suivantes doivent être définies sur les manipulators mutables :
fill(DataHolder, MergeFunction)
devrait remplacer les données de votre objet avec celles du DataHolder
donné, en utilisant le résultat de MergeFunction#merge()
.
import org.spongepowered.api.data.DataHolder;
import org.spongepowered.api.data.merge.MergeFunction;
import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;
import java.util.Optional;
@Override
public Optional<FriendsData> fill(DataHolder dataHolder, MergeFunction overlap) {
FriendsData merged = overlap.merge(this, dataHolder.get(FriendsData.class).orElse(null));
setValue(merged.friends().get());
return Optional.of(this);
}
from(DataContainer)
devrait remplacer sa valeur avec celle dans le conteneur et se retourner, sinon retourner Optional.empty()
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.DataQuery;
import org.spongepowered.cookbook.myhomes.data.Keys;
import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;
import org.spongepowered.cookbook.myhomes.data.friends.ImmutableFriendsData;
import com.google.common.collect.Maps;
import java.util.Optional;
import java.util.UUID;
@Override
public Optional<FriendsData> from(DataContainer container) {
if(container.contains(Keys.FRIENDS)) {
List<UUID> friends = container.getObjectList(Keys.FRIENDS.getQuery(), UUID.class).get();
return Optional.of(setValue(friends));
}
return Optional.empty();
}
copy()
devrait, comme son nom l’indique, retourner une copie de lui-même avec les mêmes données.
import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;
@Override
public FriendsData copy() {
return new FriendsDataImpl(getValue());
}
Types Uniques Customisés
En plus des méthodes des types uniques simples, vous devez réécrire les méthodes suivantes :
getValueGetter()
devrait passer la Value
représentant vos donnes (voir ci-dessus).
toContainer()
devrait retourner un DataContainer
représentant vos données (voir ci-dessus).
Types Composés
En considérant que les types uniques ne supportent qu’une seule valeur, les types « composés » supportent autant de valeurs que vous voulez. C’est utile lorsque plusieurs objets sont groupés, comme le FurnaceData. L’inconviénient, cependant, est qu’ils sont plus complexes à implémenter.
Pour commencer, créez tous les getters de Value
que vos données vont avoir. Pour chaque valeur, créez une méthode pour obtenir et définir l’objet brut, que vous utiliserez plus tard. Pour les données immuables, seulement les getters sont nécessaires.
Enregistrer les Valeurs
Ensuite, vous voudrez les enregistrer afin que le système basé sur les Keys puisse les référencer. Pour faire ceci, implémentez soit AbstractData#registerGettersAndSetters() si les données sont mutables, AbstractImmutableData#registerGetters() sinon.
Pour chaque valeur vous devez appeler :
registerKeyValue(Key, Supplier)
référançant le getter deValue
pour la clé donnéeregisterFieldGetter(Key, Supplier)
référançant le getter pour l’objet brut définit plus hautregisterFieldSetter(Key, Consumer)
référançant le setter ci-dessus si vous implémentez la version mutable
Nous recommandez d’utilisez la syntax ::
de Java 8 pour des fonctions Supplier
et Consumer
simples.
Exemple de Code : Implémenter des Getters et des Setters
import org.spongepowered.cookbook.myhomes.data.Keys
// registerGetters() for immutable implementation
@Override
protected void registerGettersAndSetters() {
registerKeyValue(Keys.DEFAULT_HOME, this::defaultHome);
registerKeyValue(Keys.HOMES, this::homes);
registerFieldGetter(Keys.DEFAULT_HOME, this::getDefaultHome);
registerFieldGetter(Keys.HOMES, this::getHomes);
// Only on mutable implementation
registerFieldSetter(Keys.DEFAULT_HOME, this::setDefaultHome);
registerFieldSetter(Keys.HOMES, this::setHomes);
}
fill(DataHolder, MergeFunction)
et from(DataContainer)
sont semblables à des implémentations pour des données uniques, mais en chargeant toutes nos valeurs.