Wykorzystanie DataManipulators

Ten poradnij jest dla autorów, którzy chcą pomóc w tworzeniu DataAPI (API operujące danymi) tworząc DataManipulators. Zaktualizowaną listę dostępnychDataManipulators do wdrożenia można odnaleźć w SpongeCommon Issue #8.

Do pełnego działania DataManipulator należy wykonać następujące kroki:

  1. Wdrożyć DataManipulator samodzielnie

  2. Wdrożyć ImmutableDataManipulator

Gdy te kroki zostały wykonane, należy wykonać następnie:

  1. Zarejestrować Key w KeyRegistry

  2. Wdrożyć DataProcessor

  3. Wdrożyć ValueProcessor dla każdej wartości reprezentowanej przez DataManipulator.

  4. Dodać wszystko jako wpis do rejestru SpongeSerializationRegistry.

Informacja

Upewnij się, że możesz śledzić nasze Zasady współpracy.

1. Implement the DataManipulator - Wdrażanie Manipulatora Danych

Ogólna nazwa DataManipulator określa implementacje interface z prefiksu „Sponge”. Więc aby zaimplementować interfejs HealthData, musimy zdefiniować klasę SpongeHealthData w odpowiednim pakiecie. Dla zaimplementowania DataManipulator pierw musi posiadać klasę abstrakcyjną z pakietem org.spongepowered.common.data.manipulator.mutable.common. Najbardziej ogólna jest AbstractData, ale istnieją także inne przypadki które zmniejszają właściwości DataManipulator do nawet jednej wartości.

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

Istnieją dwa typy argumentów do klasy AbstractData. Pierwszym z nich jest interfejs zaimplementowanego przez klase, a drugim interfejs zaimplementowanego przez odpowiednie ImmutableDataManipulator.

Konstruktor obiektu

W większości przypadków podczas wdrażania abstrakcyjnego Manipulatora musisz posiadać dwa konstruktory:

  • Jeden bez argumentów, które wywołuje konstruktora drugiego z wartości „domyślne”.

  • Drugiego konstruktora, który wspiera wszystkie podane wartości.

Drugi konstruktor musi

  • wywołać konstruktor AbstractData, przekazując odwołanie klasy zaimplementowanego interfejsu.

  • Upewnij się, że wartości przekazywane są prawidłowo

  • Wywołaj metodę: 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();
    }

    ...

}

Ponieważ wiemy, że obecny stan zdrowia i maksymalnego zdrowia są ograniczone wartości, to musimy upewnić się, że nie mogą być przekazywane poza granice. Aby to osiągnąć stosujemy guava Preconditions (warunki Google Preconditions), które potrzebują statycznych metod które zaimportowaliśmy.

Informacja

Nigdy nie używaj tzw. magicznych wartości (dowolnych liczb, wartości logicznych itp) w kodzie. Zamiast tego zlokalizować klasę org.spongepowered.common.data.util.DataConstants i użyć jej do dopasowania stałej - lub utworzyć jeśli to konieczne.

Uprawnienia definiowane przez interfejs

Interfejs, który wdrażamy określa niektóre metody dostępu do obiektów. Dla HealthData, to health() i maxHealth(). Każde wywołanie tych metod powinno przynieść nową „wartość”.

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

Wskazówka

Odkąd Double jest Comparable (porównywanie) to nie trzeba jawnie określać comparatora.

Jeżeli nie bieżąca wartość zostanie określona, wywołujący get() na Value (wartość) zwracana jest wtedy wartość domyślna.

Kopiowanie i Serializacja

Obydwie metody copy() i aslmmutable() nie będą działać we wdrożeniu. Dla obu po prostu trzeba zwrócić odpowiednio zmienny lub niezmienny dane manipulatora, zawierające te same dane jako bieżące wystąpienie.

Metoda toContainer() jest używana w celu serializacji. Używając MemoryDataContainer jako wynik, można je też zastosować jako magazyn instancji. DataContainer jest zasadzie mapą DataQuery dla wartości. Rozpoczynając kluczem zawsze zawiera odpowiednie DataQuery, więc szukaj po prostu klucza bezpośrednio.

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

registerGettersAndSetters()

DataManipulator zawiera również metody do pobierania i ustawiania danych za pomocą kluczy. Wdrożenie to jest obsługiwane przez AbstractData, ale musimy przedstawić które dane mają dostęp i jak. W związku z tym w metodzie registerGettersAndSetters() musimy przypisać następujące informacje dla każdej wartości:

  • zarejestrować``Supplier`` aby bezpośrednio uzyskać wartość

  • zarejestrować Consumer aby bezpośrednio ustawić wartość rejestru

  • zarejestrować Supplier<Value> aby pobrać zmienną Value (wartość)

Supplier i Consumer są funkcjonalnymi interfejsami więc mogą być używane w Java 8 Lambdas.

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);
}

Consumer zarejestrowany jako pole setter musi wykonać odpowiednie środki kontroli, by upewnić się, że dostarczona wartość jest odpowiednia. Dotyczy to zwłaszcza DataHolder, który nie akceptuje wartości ujemnych, jeśli wartość jest nieprawidłowa to IllegalArgumentException powinno zostać wyrzucone.

Wskazówka

Kryteria sprawdzające poprawność danych dla settera są tym samym co poszczególne obiekty Value, więc może przekazać do sprawdzania poprawność do wywołania this.health().set() i wystarczy ustawić this.currentHealth = wartość, jeżeli pierwszy wiersz ma wyrzucony wyjątek.

To jest to. DataManipulator powinieneś skończyć teraz.

2. Implement the ImmutableDataManipulator

Wdrażanie ImmutableDataManipulator jest podobne do wykonania co zmienny (MutableDataManipulator).

Jedyne różnice to:

  • Jako klasę wartości do DataManipulator dodaje się z nazwy ImmutableSponge

  • W zamian dziedziczy z ImmutableAbstractData

  • Zamiast metody registerGettersAndSetters() wywoływana jest registerGetters()

Podczas tworzenia ImmutableDataHolder lub ImmutableValue sprawdz czy jest sens używać ImmutableDataCachingUtil. Dla przykładu jeśli używasz WetData, który nie zawiera nic więcej niż boolean, to jest bardziej praktycznie, aby zachować tylko dwa wystąpienia buforowe ImmutableWetData - jeden dla każdej możliwej wartości. Dla Manipulatorów i wartości z wielu możliwych wartości (takie jak SigData) jednak buforowanie okazało się być zbyt zasobożerne.

Wskazówka

Aby zapobiec przypadkowym zmianom należy zadeklarować ImmutableDataManipulator jako final.

3. Dodawanie Key (klucza) do KeyRegistry (rejestru kluczy)

Następnym krokiem jest wpisanie swojego klucza Key do rejestru kluczy KeyRegistry. Aby to zrobić należy zlokalizować klasy org.spongepowered.common.data.key.KeyRegistry i znaleźć funkcje statyczną generateKeyMap(). Tutaj dodaj linię do rejestracji (i tworzenia) używanych kluczy.

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

The keyMap maps strings to Keys. The string used should be the corresponding constant name from the Keys utility class in lowercase. The Key itself is created by one of the static methods provided by KeyFactory, in most cases makeSingleKey. makeSingleKey requires first a class reference for the underlying data, which in our case is a „Double”, then a class reference for the Value type used. The third argument is the DataQuery 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. Wdróż DataProcessors

Następny jest DataProcessor. DataProcessor służy jako pomost między naszym ``DataManipulator``em a obiektami Minecrafta. Za każdym razem, gdy żądamy danych lub oferujemy je ``DataHolder``owi, który istnieje w Vanilla Minecraftcie, wywołania te są delegowane do ``DataProcessor``a lub ``ValueProcessor``a.

Dla twojej nazwy, powinieneś użyć nazwy interfejsu DataManipulator oraz dodać Procesor. A zatem dla HealthData tworzymy HealthDataProcessor.

W celu zmniejszenia standardowego kodu, DataProcessor` powinien dziedziczyć od odpowiedniej klasy abstrakcji w pakiecie ``org.spongepowered.common.data.pocessor.common. Ponieważ zdrowie może być obecne tylko przy niektórych jednostkach, możemy korzystać z „AbstractEntityDataProcessor” który jest przeznaczony dla jednostki bazowany na net.minecraft.entity.Entity. AbstractEntitySingleDataProcessor wymaga mniej pracy implementacji, ale nie może służyć jako DaneZdrowotne ponieważ zawiera więcej niż jedną wartość.

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

W zależności od tego, jakiej abstrakcji używasz, metody które musisz zaimplementować mogą się znacznie różnić w zależności od tego ile implementacji można wykonać w ramach klasy abstrakcyjnej. Generalnie, metody mogą być kategoryzowane.

Wskazówka

It is possible to create multiple DataProcessors for the same data. If vastly different DataHolders should be supported (for example both a TileEntity and a matching ItemStack), it may be beneficial to create one processor for each type of DataHolder in order to make full use of the provided abstractions. Make sure you follow the package structure for items, tileentities and entities.

Metody sprawdzania poprawności

Always return a boolean value. If the method is called supports() it should perform a general check if the supplied target generally supports the kind of data handled by our DataProcessor.

For our HealthDataProcessor supports() is implemented by the AbstractEntityDataProcessor. Per default, it will return true if the supplied argument is an instance of the class specified when calling the super() constructor.

Instead, we are required to provide a doesDataExist() method. Since the abstraction does not know how to obtain the data, it leaves this function to be implemented. As the name says, the method should check if the data already exists on the supported target. For the HealthDataProcessor, this always returns true, since every living entity always has health.

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

Metody ustawiacza

A setter method receives a DataHolder of some sort and some data that should be applied to it, if possible.

The DataProcessor interface defines a set() method accepting a DataHolder and a DataManipulator which returns a DataTransactionResult. Depending on the abstraction class used, some of the necessary functionality might already be implemented.

In this case, the AbstractEntityDataProcessor takes care of most of it and just requires a method to set some values to return true if it was successful and false if it was not. All checks if the DataHolder supports the Data is taken care of, the abstract class will just pass a Map mapping each Key from the DataManipulator to its value and then construct a DataTransactionResult depending on whether the operation was successful or not.

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;
}

Wskazówka

To understand DataTransactionResult s, check the corresponding docs page and refer to the DataTransactionResult.Builder docs to create one.

Ostrzeżenie

Especially when working with ItemStacks it is likely that you will need to deal with NBTTagCompounds directly. Many NBT keys are already defined as constants in the org.spongepowered.common.data.util.NbtDataUtil class. If your required key is not there, you need to add it in order to avoid «magic values» in the code.

Metody usuwania

The remove() method attempts to remove data from the DataHolder and returns a DataTransactionResult.

Removal is not abstracted in any abstract DataProcessor as the abstractions have no way of knowing if the data is always present on a compatible DataHolder (like WetData or HealthData) or if it may or may not be present (like LoreData). If the data is always present, remove() must always fail. If it may or may not be present, remove() should remove it. In such cases the doesDataExist() method should be overridden.

Since a living entity always has health, HealthData is always present and removal therefore not supported. Therefore we just return failNoData() and do not override the doesDataExist() method.

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

Metody getter

Getter methods obtain data from a DataHolder and return an optional DataManipulator. The DataProcessor interface specifies the methods from() and createFrom(), the difference being that from() will return Optional.empty() if the data holder is compatible, but currently does not contain the data, while createFrom() will provide a DataManipulator holding default values in that case.

Again, AbstractEntityDataProcessor will provide most of the implementation for this and only requires a method to get the actual values present on the DataHolder. This method is only called after supports() and doesDataExist() both returned true, which means it is run under the assumption that the data is present.

Ostrzeżenie

If the data may not always exist on the target DataHolder, e.g. if the remove() function may be successful (see above), it is imperative that you override the doesDataExist() method so that it returns true if the data is present and false if it is not.

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);
}

Filtruj Metody

A filler method is different from a getter method in that it receives a DataManipulator to fill with values. These values either come from a DataHolder or have to be deserialized from a DataContainer. The method returns Optional.empty() if the DataHolder is incompatible.

AbstractEntityDataProcessor already handles filling from DataHolders by creating a DataManipulator from the holder and then merging it with the supplied manipulator, but the DataContainer deserialization it can not provide.

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();
}

The fill() method is to return an Optional of the altered healthData, if and only if all required data could be obtained from the DataContainer.

Inne metody

W zależności od zastosowanej abstrakcyjnej superklasy mogą być wymagane inne metody. Na przykład `` AbstractEntityDataProcessor`` musi stworzyć instancje `` DataManipulator`` w różnych punktach. Nie może tego zrobić, ponieważ nie zna klasy implementacji ani konstruktora. Dlatego wykorzystuje abstrakcyjną funkcję, która musi zostać zapewniona przez ostateczną implementację. To nie robi nic więcej niż tworzenie „DataManipulator” z domyślnymi danymi.

If you implemented your DataManipulator as recommended, you can just use the no-args constructor.

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

5. Implementowanie ValueProcessors

Not only a DataManipulator may be offered to a DataHolder, but also a keyed Value on its own. Therefore, you need to provide at least one ValueProcessor for every Key present in your DataManipulator. A ValueProcessor is named after the constant name of its Key in the Keys class in a fashion similar to its DataQuery. The constant name is stripped of underscores, used in upper camel case and then suffixed with ValueProcessor.

A ValueProcessor should always inherit from AbstractSpongeValueProcessor, which already will handle a portion of the supports() checks based on the type of the DataHolder. For Keys.HEALTH, we’ll create and construct HealthValueProcessor as follows.

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

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

    [...]
}

Now the AbstractSpongeValueProcessor will relieve us of the necessity to check if the value is supported. It is assumed to be supported if the target ValueContainer is of the type EntityLivingBase.

Wskazówka

Aby uzyskać dokładniejszą kontrolę nad tym, co obiekty `` EntityLivingBase`` są obsługiwane, metoda `` supports (EntityLivingBase) `` może zostać zastąpiona.

Ponownie, większość prac jest wykonywana przez klasę abstrakcji. Musimy tylko wdrożyć dwie metody pomocnicze do tworzenia wartości `` Value`` i jej niezmiennego odpowiednika oraz trzy metody uzyskiwania, ustawiania i usuwania danych.

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());
}

Ponieważ nie jest możliwe, aby `` EntityLivingBase`` nie miał zdrowia, ta metoda nigdy nie zwróci `` 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;
}

Metoda `` set () `` zwróci wartość logiczną wskazującą, czy wartość mogła zostać pomyślnie ustawiona. Ta implementacja odrzuci wartości spoza granic używanych w naszych metodach budowania wartości powyżej.

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

Ponieważ dane są zawsze obecne, próby usunięcia go zakończą się niepowodzeniem.

6. Rejestruj Procesory

Aby Sponge mogło korzystać z naszych manipulatorów i procesorów, musimy je zarejestrować. Odbywa się to w klasie org.spongepowered.common.data.SpongeSerializationRegistry. W metodzie setupSerialization są dwa duże bloki rejestracji do których dodajemy nasze procesory.

DataProcessors

`` DataProcessor`` jest rejestrowany obok interfejsu i klas implementacji `` DataManipulator``, który obsługuje. Dla każdej pary zmiennych / niezmiennych `` DataManipulator`` co najmniej jeden `` DataProcessor`` musi być zarejestrowany.

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

ValueProcessors

Value processors are registered at the bottom of the very same function. For each Key multiple processors can be registered by subsequent calls of the registerValueProcessor() method.

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

Dodatkowe informacje

With Data being a rather abstract concept in Sponge, it is hard to give general directions on how to acquire the needed data from the Minecraft classes itself. It may be helpful to take a look at already implemented processors similar to the one you are working on to get a better understanding of how it should work.

If you are stuck or are unsure about certain aspects, go visit the #spongedev IRC channel, the forums, or open up an Issue on GitHub. Be sure to check the Data Processor Implementation Checklist for general contribution requirements.