Реализация DataManipulators
Это руководство для тех, кто хочет помочь с реализацией Data API, создав DataManipulators. Обновленный список DataManipulators, который должен быть реализован, можно найти в SpongeCommon Issue #8.
To fully implement a DataManipulator these steps must be followed:
Реализовать
DataManipulator
Implement the ImmutableDataManipulator
По завершению этих шагов, необходимо также выполнить следующее:
Register the Key in the
KeyRegistryModule
Реализовать
DataProcessor
Implement the
ValueProcessor
for each Value being represented by theDataManipulator
If the data applies to a block, several methods must also be mixed in to the block.
Примечание
Убедитесь, что вы следуете нашим правилам.
The following snippet shows the imports/paths for some classes in SpongeCommon that you will need:
import org.spongepowered.common.data.DataProcessor;
import org.spongepowered.common.data.ValueProcessor;
import org.spongepowered.common.data.manipulator.immutable.entity.ImmutableSpongeHealthData;
import org.spongepowered.common.data.manipulator.mutable.common.AbstractData;
import org.spongepowered.common.data.manipulator.mutable.entity.SpongeHealthData;
import org.spongepowered.common.data.processor.common.AbstractEntityDataProcessor;
import org.spongepowered.common.util.Constants;
import org.spongepowered.common.data.util.NbtDataUtil;
import org.spongepowered.common.registry.type.data.KeyRegistryModule;
1. Реализовать DataManipulator
The naming convention for DataManipulator
implementations is the name of the interface prefixed with «Sponge».
So to implement the HealthData interface, we create a class named SpongeHealthData
in the appropriate package.
For implementing the DataManipulator
first have it extend an appropriate abstract class from the
org.spongepowered.common.data.manipulator.mutable.common
package. The most generic there is AbstractData
but there are also abstractions that reduce boilerplate code even more for some special cases like
DataManipulator
s only containing a single value.
public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {
[...]
}
Класс AbstractData содержит два типовых аргумента. Первый — интерфейс, реализованный этим классом, второй — интерфейс, реализуемый соответствующим ImmutableDataManipulator
.
Конструктор
In most cases while implementing an abstract DataManipulator
you need to have two constructors:
Один без аргументов (no-args), который вызывает второй конструктор со значениями «по умолчанию»
Второй конструктор, который принимает все поддерживаемые значения.
Второй конструктор должен
сделать вызов конструктора
AbstractData
, передав ссылку на класс для реализованного интерфейса.убедиться, что передаваемые значения действительны
вызвать метод
registerGettersAndSetters()
import static com.google.common.base.Preconditions.checkArgument;
public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {
private double health;
private double maxHealth;
public SpongeHealthData() {
this(20D, 20D);
}
public SpongeHealthData(double health, double maxHealth) {
super(HealthData.class);
checkArgument(maxHealth > 0);
this.health = health;
this.maxHealth = maxHealth;
registerGettersAndSetters();
}
[...]
}
Since we know that both current health and maximum health are bounded values, we need to make sure no values
outside of these bounds can be passed. To achieve this, we use guava’s Preconditions
of which we import the
required methods statically.
Примечание
Never use so-called magic values (arbitrary numbers, booleans etc.) in your code. Instead, locate the
DataConstants
class and use a fitting constant - or create one, if necessary.
Методы определенные интерфейсом
The interface we implement specifies some methods to access Value objects. For HealthData
, those are
HealthData#health() and HealthData#maxHealth(). Every call to those should yield a new Value
.
public MutableBoundedValue<Double> health() {
return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
.minimum(0)
.maximum(this.maxHealth)
.defaultValue(this.maxHealth)
.actualValue(this.health)
.build();
}
Совет
Поскольку Double
является Comparable
, нам не нужно явно указывать компаратор.
If no current value is specified, calling BaseValue#get() on the Value
returns the default value.
Копирование и Сериализация
The two methods DataManipulator#copy() and DataManipulator#asImmutable() are not much work to implement. For both you just need to return a mutable or an immutable data manipulator respectively, containing the same data as the current instance.
The method DataSerializable#toContainer() is used for serialization purposes. Use
DataContainer#createNew() as the result and apply to it the values stored within this instance.
A DataContainer is basically a map mapping DataQuerys to values. Since a Key always
contains a corresponding DataQuery
, just use those by passing the Key
directly.
public DataContainer toContainer() {
return super.toContainer()
.set(Keys.HEALTH, this.health)
.set(Keys.MAX_HEALTH, this.maxHealth);
}
registerGettersAndSetters()
DataManipulator
также предоставляет методы для получения и установки данных с использованием ключей. Реализация для этого обрабатывается AbstractData
, но мы должны указать, какие данные он может получить и как. Поэтому в методе registerGettersAndSetters()
нам нужно сделать следующее для каждого значения:
register a Supplier to directly get the value
register a Consumer to directly set the value
зарегистрировать
Supplier<Value>``для получения изменяемого ``Value
Supplier
и Consumer
являются функциональными интерфейсами, поэтому для них могут использоваться лямбда-выражения из Java 8.
private SpongeHealthData setCurrentHealthIfValid(double value) {
if (value >= 0 && value <= (double) Float.MAX_VALUE) {
this.health = value;
} else {
throw new IllegalArgumentException("Invalid value for current health");
}
return this;
}
private SpongeHealthData setMaximumHealthIfValid(double value) {
if (value >= 0 && value <= (double) Float.MAX_VALUE) {
this.maxHealth = value;
} else {
throw new IllegalArgumentException("Invalid value for maximum health");
}
return this;
}
private void registerGettersAndSetters() {
registerFieldGetter(Keys.HEALTH, () -> this.health);
registerFieldSetter(Keys.HEALTH, this::setCurrentHealthIfValid);
registerKeyValue(Keys.HEALTH, this::health);
registerFieldGetter(Keys.MAX_HEALTH, () -> this.maxHealth);
registerFieldSetter(Keys.MAX_HEALTH, this::setMaximumHealthIfValid);
registerKeyValue(Keys.MAX_HEALTH, this::maxHealth);
}
The Consumer
registered as field setter must perform the adequate checks to make sure the supplied value is valid.
This applies especially for DataHolders which won’t accept negative values. If a value is invalid, an
IllegalArgumentException
should be thrown.
Совет
The validity criteria for those setters are the same as for the respective Value
object, so you might delegate
the validity check to a call of this.health().set()
and just set this.health = value
if the first
line has not thrown an exception yet.
Вот и всё. DataManipulator
должен быть выполнен.
2. Реализация ImmutableDataManipulator
Implementing the ImmutableDataManipulator is similar to implementing the mutable one.
Единственные отличия:
Имя класса формируется путем добавления префикса
ImmutableSponge
к имени изменяемогоDataManipulator
Наследование от
ImmutableAbstractData
Метод
registerGettersAndSetters()
заменён наregisterGetters()
When creating ImmutableDataHolder
s or ImmutableValue
s, check if it makes sense to use the
ImmutableDataCachingUtil
. For example, if you have WetData
which contains nothing more than a boolean, it
is more feasible to retain only two cached instances of ImmutableWetData
- one for each possible value. For
manipulators and values with many possible values (like SignData
) however, caching is proven to be too expensive.
Совет
Вы должны объявить поля ImmutableDataManipulator
как final
, чтобы предотвратить случайные изменения.
3. Register the Key in the KeyRegistryModule
The next step is to register your Keys to the Keys. To do so, locate the
KeyRegistryModule
class and find the registerDefaults()
method.
There add a line to register (and create) your used keys.
import static org.spongepowered.api.data.DataQuery.of;
this.register("health", Key.builder()
.type(TypeTokens.BOUNDED_DOUBLE_VALUE_TOKEN)
.id("health")
.name("Health")
.query(of("Health"))
.build());
this.register("max_health", Key.builder()
.type(TypeTokens.BOUNDED_DOUBLE_VALUE_TOKEN)
.id("max_health")
.name("Max Health")
.query(of("MaxHealth"))
.build());
The register(Key)
method registers your Key
s for later use. The string used for the id should be the
corresponding constant name from the Keys
utility class in lowercase. The Key
itself is created by using the
Key.Builder provided by the Key#builder() method. You have to set a TypeToken
, an id
,
human readable name
and a DataQuery
.
The DataQuery
is 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. Реализация DataProcessors
Next up is the DataProcessor
. A DataProcessor
serves as a bridge between our DataManipulator
and
Minecraft’s objects. Whenever any data is requested from or offered to DataHolders
that exist in Vanilla
Minecraft, those calls end up being delegated to a DataProcessor
or a ValueProcessor
.
For your name, you should use the name of the DataManipulator
interface and append Processor
. Thus, for
HealthData
we create a HealthDataProcessor
.
In order to reduce boilerplate code, the DataProcessor
should inherit from the appropriate abstract class in
the org.spongepowered.common.data.processor.common
package. Since health can only be present on certain
entities, we can make use of the AbstractEntityDataProcessor
which is specifically targeted at Entities
based on net.minecraft.entity.Entity
. AbstractEntitySingleDataProcessor
would require less
implementation work, but cannot be used as HealthData
contains more than just one value.
public class HealthDataProcessor
extends AbstractEntityDataProcessor<EntityLivingBase, HealthData, ImmutableHealthData> {
public HealthDataProcessor() {
super(EntityLivingBase.class);
}
[...]
}
Depending on which abstraction you use, the methods you have to implement may differ greatly, depending on how much implementation work already could be done in the abstract class. Generally, the methods can be categorized.
Совет
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.
Методы проверки
Always return a boolean value. If any of the supports(target)
methods is called it should perform a general check if
the supplied target generally supports the kind of data handled by our DataProcessor
. Based on your level of
abstraction you might not have to implement it at all, if you have to just implement the most specific one, as the more
generic ones usually delegate to them.
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.
Вместо этого мы должны предоставить метод doesDataExist()
. Поскольку абстракция не знает, как получить эти данные, она оставляет эту функцию для реализации. Как видно из его названия, метод должен проверить, существуют ли уже данные на поддерживаемой цели. Для HealthDataProcessor
он всегда возвращается с результатом «истина», потому что каждое живое существо всегда обладает здоровьем.
@Override
protected boolean doesDataExist(EntityLivingBase entity) {
return true;
}
Setter Methods
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.
@Override
protected boolean set(EntityLivingBase entity, Map<Key<?>, Object> keyValues) {
entity.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH)
.setBaseValue(((Double) keyValues.get(Keys.MAX_HEALTH)).floatValue());
float health = ((Double) keyValues.get(Keys.HEALTH)).floatValue();
entity.setHealth(health);
return true;
}
Совет
To understand DataTransactionResults, check the corresponding docs page and refer to the DataTransactionResult.Builder docs to create one.
Предупреждение
Especially when working with ItemStacks it is likely that you will need to deal with
NBTTagCompound
s directly. Many NBT keys are already defined as constants in the NbtDataUtil
class.
If your required key is not there, you need to add it in order to avoid „magic values“ in the code.
Removal Method
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.
Since a living entity always has health, HealthData
is always present and removal therefore not supported.
Therefore we just return DataTransactionResult#failNoData().
@Override
public DataTransactionResult remove(DataHolder dataHolder) {
return DataTransactionResult.failNoData();
}
Getter Methods
Методы получения берут данные от DataHolder
и возвращают опциональный DataManipulator
. Интерфейс DataProcessor
определяет методы from()
и createFrom()
, разница между которыми состоит в том, что from()
вернет Optional.empty()
, если держатель данных совместим, но в данный момент не содержит данных, a createFrom()
предоставит DataManipulator
, содержащий значения по умолчанию в данном случае.
Опять же, AbstractEntityDataProcessor
будет обеспечивать большую часть реализации этого, и требует только метода, обеспечивающего присутствие фактических значений в DataHolder
. Этот метод вызывается только после того, как supports()
и doesDataExist()
оба возвращаются с результатом «истина», а это означает, что он действует, предполагая присутствие данных.
Предупреждение
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 implement the doesDataExist()
method so that it returns true
if the data is present and false
if it is not.
@Override
protected Map<Key<?>, ?> getValues(EntityLivingBase entity) {
final double health = entity.getHealth();
final double maxHealth = entity.getMaxHealth();
return ImmutableMap.of(Keys.HEALTH, health, Keys.MAX_HEALTH, maxHealth);
}
Filler Methods
Метод заполнения отличается от метода получения в том, что он принимает DataManipulator
для заполнения значениями. Эти значения или поступают от DataHolder
, или должны быть десериализованы из DataContainer
. Этот метод вернется с результатом Optional.empty()
, если DataHolder
несовместим.
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
cannot provide.
@Override
public Optional<HealthData> fill(DataContainer container, HealthData healthData) {
if (!container.contains(Keys.MAX_HEALTH.getQuery()) || !container.contains(Keys.HEALTH.getQuery())) {
return Optional.empty();
}
healthData.set(Keys.MAX_HEALTH, getData(container, Keys.MAX_HEALTH));
healthData.set(Keys.HEALTH, getData(container, Keys.HEALTH));
return Optional.of(healthData);
}
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
.
Other Methods
В зависимости от используемого абстрактного суперкласса могут потребоваться некоторые другие методы. Например, процесcору «AbstractEntityDataProcessor» нужно создать экземпляры «DataManipulator» в разных точках. Он не может этого сделать, поскольку не знает ни класса реализации, ни используемого конструктора. Поэтому он задействует абстрактную функцию, которую должна обеспечить финальная реализация. Это не что иное, как создание «DataManipulator» с данными по умолчанию.
If you implemented your DataManipulator
as recommended, you can just use the no-args constructor.
@Override
protected HealthData createManipulator() {
return new SpongeHealthData();
}
5. Implement the 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);
}
[...]
}
Теперь «AbstractSpongeValueProcessor» избавит нас от необходимости проверять, поддерживается ли это значение. Предполагается, что оно поддерживается, если целевой «ValueContainer» имеет тип «EntityLivingBase».
Совет
For a more fine-grained control over what EntityLivingBase
objects are supported, the
supports(EntityLivingBase)
method can be overridden.
Опять же, большая часть работы выполняется классом абстракции. Нам просто нужно реализовать два вспомогательных метода для создания «Value» и его неизменяемого прототипа и три метода для получения, установки и удаления данных.
@Override
protected MutableBoundedValue<Double> constructValue(Double health) {
return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
.minimum(0D)
.maximum(((Float) Float.MAX_VALUE).doubleValue())
.defaultValue(20D)
.actualValue(health)
.build();
}
@Override
protected ImmutableBoundedValue<Double> constructImmutableValue(Double value) {
return constructValue(value).asImmutable();
}
@Override
protected Optional<Double> getVal(EntityLivingBase container) {
return Optional.of((double) container.getHealth());
}
Since it is impossible for an EntityLivingBase
to not have health, this method will never return
Optional.empty()
.
@Override
protected boolean set(EntityLivingBase container, Double value) {
if (value >= 0 && value <= (double) Float.MAX_VALUE) {
container.setHealth(value.floatValue());
return true;
}
return false;
}
Метод set()
возвращает логическое значение, которое указывает, может ли это значение быть успешно установлено. Эта реализация будет отклонять значения за пределами границ, используемых в наших методах построения значений, описанных выше.
@Override
public DataTransactionResult removeFrom(ValueContainer<?> container) {
return DataTransactionResult.failNoData();
}
Since the data is guaranteed to be always present, attempts to remove it will just fail.
6. Register Processors
In order for Sponge to be able to use our manipulators and processors, we need to register them. This is done in the
DataRegistrar
class. In the setupSerialization()
method there are two large blocks of registrations to which we
add our processors.
DataProcessors
DataProcessor
зарегистрирован рядом с интерфейсом и классами реализации манипулятора DataManipulator
, который он обрабатывает. Для каждой пары изменяемых / неизменяемых манипуляторов DataManipulator
должен быть зарегистрирован хотя бы один DataProcessor
.
DataUtil.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
ImmutableHealthData.class, ImmutableSpongeHealthData.class,
new HealthDataProcessor());
ValueProcessors
Процессоры значений зарегистрированы в нижней части той же самой функции. Для каждого «Key» могут быть зарегистрированы несколько процессоров путем последовательных вызовов метода «registerValueProcessor()».
DataUtil.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
DataUtil.registerValueProcessor(Keys.MAX_HEALTH, new MaxHealthValueProcessor());
Реализация Block Data
Данные блока несколько отличаются от других типов данных, поскольку они реализованы путем смешивания с самим блоком. Существуют несколько методов в org.spongepowered.mixin.core.block.MixinBlock
, которые должны быть переопределены для реализации данных для блоков.
@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()
должен возвращать новый BlockState
с данными из ``ImmutableDataManipulator`“, примененного к нему. Если манипулятор не поддерживается напрямую, метод должен делегироваться в суперкласс.
@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 Key
s.
@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);
}
Finally, getManipulators()
should return a list of all ImmutableDataManipulator
s the block supports, along with
the current values for the provided IBlockState
. It should include all ImmutableDataManipulator
s from the
superclass.
@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();
}
Further Information
Поскольку Data
является довольно абстрактным понятием в Sponge, трудно дать общие указания о том, как получить необходимые данные из самих классов Minecraft. Может быть полезно взглянуть на уже реализованные процессоры, аналогичные тому, над которым вы работаете, чтобы лучше понять, как он должен работать.
If you are stuck or are unsure about certain aspects, go visit the #spongedev
IRC channel,
the #dev
channel on Discord, the forums, or open up an Issue on GitHub.
Be sure to check the Data Processor Implementation Checklist
for general contribution requirements.