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ć
DataManipulator
samodzielnieWdrożyć
ImmutableDataManipulator
Gdy te kroki zostały wykonane, należy wykonać następnie:
Zarejestrować
Key
wKeyRegistry
Wdrożyć
DataProcessor
Wdrożyć
ValueProcessor
dla każdej wartości reprezentowanej przezDataManipulator
.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ść 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
DataManipulator
dodaje się z nazwyImmutableSponge
W zamian dziedziczy z
ImmutableAbstractData
Zamiast 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 Key
s. 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 DataProcessor
s for the same data. If vastly different DataHolder
s
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 ItemStack
s it is likely that you will need to deal with NBTTagCompound
s
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.