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:
Wdrożyć
DataManipulatorsamodzielnieWdrożyć
ImmutableDataManipulator
Gdy te kroki zostały wykonane, należy wykonać następnie:
Zarejestrować
KeywKeyRegistryWdrożyć
DataProcessorWdrożyć
ValueProcessordla każdej wartości reprezentowanej przezDataManipulator.Dodać wszystko jako wpis do rejestru
SpongeSerializationRegistry.
Jeśli data odnosi się do bloku, kilka metod również musi być zamieszanych w blok.
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 ``DataManipulator``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ć
Consumeraby bezpośrednio ustawić wartość rejestruzarejestrować
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
DataManipulatordodaje się z nazwyImmutableSpongeW zamian dziedziczy z
ImmutableAbstractDataZamiast metody
registerGettersAndSetters()wywoływana jestregisterGetters()
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());
Implementowanie bloków danych
Block data is somewhat different from other types of data in that it is implemented by mixing in to the block itself.
There are several methods in org.spongepowered.mixin.core.block.MixinBlock that must be overridden to implement
data for blocks.
@Mixin(BlockHorizontal.class)
public abstract class MixinBlockHorizontal extends MixinBlock {
[...]
}
supports() should return true if either the ImmutableDataManipulator interface is assignable from the
Class passed in as the argument, or the superclass supports it.
@Override
public boolean supports(Class<? extends ImmutableDataManipulator<?, ?>> immutable) {
return super.supports(immutable) || ImmutableDirectionalData.class.isAssignableFrom(immutable);
}
getStateWithData() should return a new BlockState with the data from the ImmutableDataManipulator applied
to it. If the manipulator is not directly supported, the method should delegate to the superclass.
@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() is the equivalent of getStateWithData(), but works with single Keys.
@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);
}
W końcu,``getManipulators()`` powinno zwrócić listę dla wszystkich ImmutableDataManipulators bloków wsparcia, wraz z obecnymi wartościami dla zapewnionych IBlockState. Powinno również zawierać wszystkie ImmutableDataManipulators z superklas.
@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();
}
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.