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 lui permettra d’être accédé 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 BoundedValue 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 Values.

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")
      .buildAndRegister(myPluginContainer);
}

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

  • Comparable

  • 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 du , vous devrez override 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 DataManipulator#registerGettersAndSetters(), soit ImmutableDataManipulator#registerGetters() selon si les données sont mutables ou non.

Pour chaque valeur vous devez appeler :

  • registerKeyValue(Key, Supplier) référançant le getter de Value pour la clé donnée

  • registerFieldGetter(Key, Supplier) référançant le getter pour l’objet brut définit plus haut

  • registerFieldSetter(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.