Реализация DataManipulators
Это руководство для тех, кто хочет помочь с реализацией Data API, создав DataManipulators. Обновленный список DataManipulators, который должен быть реализован, можно найти в SpongeCommon Issue #8.
Чтобы полностью реализовать DataManipulator
, необходимо выполнить следующие шаги:
Реализовать
DataManipulator
Реализовать
ImmutableDataManipulator
По завершению этих шагов, необходимо также выполнить следующее:
Зарегистрировать
Key
вKeyRegistry
Реализовать
DataProcessor
Реализовать
ValueProcessor
для каждого значения, представленногоDataManipulator
Зарегистрировать всё в
SpongeSerializationRegistry
If the data applies to a block, several methods must also be mixed in to the block.
Примечание
Убедитесь, что вы следуете нашим правилам.
1. Реализовать DataManipulator
Соглашение об именах для реализаций DataManipulator
является именем интерфейса с префиксом «Sponge». Поэтому для реализации интерфейса HealthData
мы создаем класс с именем SpongeHealthData
в соответствующем пакете. Для реализации DataManipulator
сначала нужно расширить соответствующий абстрактный класс из пакета org.spongepowered.common.data.manipulator.mutable.common
. Самым общим является AbstractData
, но есть абстракции, которые еще более сокращают шаблонный код для некоторых специальных случаев, например для DataManipulator
-ов, содержащих только одно значение.
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 {
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();
}
...
}
Поскольку мы знаем, что значения current health и maximum health являются ограниченными, мы должны убедиться, что любые значения за пределами этих границ не передадутся. Для достижения этого мы используем guava Preconditions
из которых мы статически импортируем необходимые методы.
Примечание
Никогда не используйте в своем коде так называемые магические значения (произвольные числа, логические значения и т.д.). Вместо этого найдите класс org.spongepowered.common.data.util.DataConstants
и используйте подходящую константу. Или создайте её, если это необходимо.
Методы определенные интерфейсом
Интерфейс, который мы реализуем, определяет некоторые методы для доступа к объектам Value
. Для HealthData
это health()
и maxHealth()
. Каждый вызов к ним должен дать новое Value
.
public MutableBoundedValue<Double> health() {
return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
.minimum(DataConstants.MINIMUM_HEALTH)
.maximum(this.maximumHealth)
.defaultValue(this.maximumHealth)
.actualValue(this.currentHealth)
.build();
}
Совет
Поскольку Double
является Comparable
, нам не нужно явно указывать компаратор.
Если текущее значение не указано, вызов get()
в Value
возвращает значение по умолчанию.
Копирование и Сериализация
Два метода copy()` и asImmutable()
не сложны для реализации. В обоих методах вам необходимо вернуть изменяемый либо не изменяемый «data manipulator» соответственно, содержащий те же значения что и текущий объект.
Метод toContainer()
используется для целей сериализации. Используйте в качестве результата MemoryDataContainer
и примените к нему значения, хранящиеся в этом экземпляре. DataContainer
является в основном отображением карты DataQuery
-ов в значения. Так как Key
всегда содержит соответствующий DataQuery
, просто используйте их, передавая непосредственно Key
.
public DataContainer toContainer() {
return new MemoryDataContainer()
.set(Keys.HEALTH, this.currentHealth)
.set(Keys.MAX_HEALTH, this.maximumHealth);
}
registerGettersAndSetters()
DataManipulator
также предоставляет методы для получения и установки данных с использованием ключей. Реализация для этого обрабатывается AbstractData
, но мы должны указать, какие данные он может получить и как. Поэтому в методе registerGettersAndSetters()
нам нужно сделать следующее для каждого значения:
зарегистрировать
Supplier
для непосредственного получения значениязарегистрировать ``Consumer``для непосредственной установки значения
зарегистрировать
Supplier<Value>``для получения изменяемого ``Value
Supplier
и Consumer
являются функциональными интерфейсами, поэтому для них могут использоваться лямбда-выражения из Java 8.
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
, зарегистрированный как установщик поля, должен выполнить соответствующие проверки, чтобы убедиться, что поставляемое значение допустимо. Это особенно касается DataHolder
-ов, которые не принимают отрицательных значений. Если значение недопустимо, должно быть выброшено исключение IllegalArgumentException
.
Совет
Критерии достоверности для этих установщиков такие же, как для соответствующего объекта Value
, поэтому вы можете делегировать проверку действительности вызову this.health().set()
и просто установить this.currentHealth = value
, если первая строка ещё не выбрасывает исключение.
Вот и всё. DataManipulator
должен быть выполнен.
2. Реализация ImmutableDataManipulator
Реализация ImmutableDataManipulator
аналогична реализации mutable.
Единственные отличия:
Имя класса формируется путем добавления префикса
ImmutableSponge
к имени изменяемогоDataManipulator
Наследование от
ImmutableAbstractData
Метод
registerGettersAndSetters()
заменён наregisterGetters()
При создании ImmutableDataHolder
-ов или ImmutableValue
-й, проверьте, имеет ли смысл использовать ImmutableDataCachingUtil
. Например, если у вас есть WetData
, который не содержит ничего, кроме логическое значение, более целесообразно будет сохранить только два кэшированных экземпляра ImmutableWetData
— по одному для каждого возможного значения. Однако для манипуляторов и значений со многими возможными значениями (например, SignData
) кэширование окажется слишком затратным.
Совет
Вы должны объявить поля ImmutableDataManipulator
как final
, чтобы предотвратить случайные изменения.
3. Регистрация Key в KeyRegistry
Следующим шагом является регистрация вашего Key
-ев в `` KeyRegistry``. Чтобы сделать это, найдите класс org.spongepowered.common.data.key.KeyRegistry
и найдите статическую функцию generateKeyMap()
. Там добавьте строку для регистрации (и создания) используемых ключей.
keyMap.put("health"), makeSingleKey(Double.class, MutableBoundedValue.class, of("Health")));
keyMap.put("max_health", makeSingleKey(Double.class, MutableBoundedValue.class, of("MaxHealth")));
keyMap
сопоставляет строки с Key
-ми. Используемая строка должна быть соответствующим именем константы из служебного класса Keys
в нижнем регистре. Ключ Key
создается одним из статических методов, предоставляемых KeyFactory
, в большинстве случаев makeSingleKey
. makeSingleKey
требует сначала ссылку на класс для базовых данных, которая в нашем случае является «Double», а затем ссылку на класс для типа используемого Value
. Третий аргумент — это DataQuery
, используемый для сериализации. Он создается из статически импортируемого метода DataQuery.of()
, принимающего строку. Эта строка также должна быть именем константы, с линией подчеркивания и заглавной буквой, в стиле UpperCamelCase.
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 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.
Вместо этого мы должны предоставить метод doesDataExist()
. Поскольку абстракция не знает, как получить эти данные, она оставляет эту функцию для реализации. Как видно из его названия, метод должен проверить, существуют ли уже данные на поддерживаемой цели. Для HealthDataProcessor
он всегда возвращается с результатом «истина», потому что каждое живое существо всегда обладает здоровьем.
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.
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;
}
Совет
To understand DataTransactionResult
s, check the corresponding docs page and refer to the
DataTransactionResult.Builder docs to create one.
Предупреждение
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.
Removal Method
The remove()
method attempts to remove data from the DataHolder
and returns a DataTransactionResult
.
Удаление не абстрагируется ни в каком абстрактном DataProcessor
, поскольку абстракции не имеют возможности узнать, всегда ли данные присутствуют на совместимом DataHolder
(например, `` WetData`` или HealthData
), а также могут или не могут они присутствовать (например, LoreData
). Если данные всегда присутствуют, remove()
должен всегда закончиться неуспехом. Если они могут или не могут присутствовать, remove()
должен удалить их. В таких случаях метод doesDataExist()
должен быть переопределен.
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();
}
Getter Methods
Методы получения берут данные от DataHolder
и возвращают опциональный DataManipulator
. Интерфейс DataProcessor
определяет методы from()
и createFrom()
, разница между которыми состоит в том, что from()
вернет Optional.empty()
, если держатель данных совместим, но в данный момент не содержит данных, a createFrom()
предоставит DataManipulator
, содержащий значения по умолчанию в данном случае.
Опять же, AbstractEntityDataProcessor
будет обеспечивать большую часть реализации этого, и требует только метода, обеспечивающего присутствие фактических значений в DataHolder
. Этот метод вызывается только после того, как supports()
и doesDataExist()
оба возвращаются с результатом «истина», а это означает, что он действует, предполагая присутствие данных.
Предупреждение
Если данные не всегда могут существовать в целевом DataHolder
, например, при успешной функции remove ()
(см. выше), вам необходимо переопределить метод doesDataExist()
так, чтобы он возвращался с результатом «истина» при наличии данных и «ложь» при их отсутствии.
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);
}
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
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
.
Other Methods
В зависимости от используемого абстрактного суперкласса могут потребоваться некоторые другие методы. Например, процесcору «AbstractEntityDataProcessor» нужно создать экземпляры «DataManipulator» в разных точках. Он не может этого сделать, поскольку не знает ни класса реализации, ни используемого конструктора. Поэтому он задействует абстрактную функцию, которую должна обеспечить финальная реализация. Это не что иное, как создание «DataManipulator» с данными по умолчанию.
If you implemented your DataManipulator
as recommended, you can just use the no-args constructor.
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» и его неизменяемого прототипа и три метода для получения, установки и удаления данных.
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());
}
Since it is impossible for an EntityLivingBase
to not have health, this method will never return
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;
}
Метод set()
возвращает логическое значение, которое указывает, может ли это значение быть успешно установлено. Эта реализация будет отклонять значения за пределами границ, используемых в наших методах построения значений, описанных выше.
public DataTransactionResult removeFrom(ValueContainer<?> container) {
return DataTransactionBuilder.failNoData();
}
Since the data is guaranteed to be always present, attempts to remove it will just fail.
6. Register Processors
Чтобы Sponge мог использовать наши манипуляторы и процессоры, нам необходимо их зарегистрировать. Это осуществляется в классе org.spongepowered.common.data.SpongeSerializationRegistry
. В методе setupSerialization
есть два больших блока регистрации, к которым мы добавляем наши процессоры.
DataProcessors
DataProcessor
зарегистрирован рядом с интерфейсом и классами реализации манипулятора DataManipulator
, который он обрабатывает. Для каждой пары изменяемых / неизменяемых манипуляторов DataManipulator
должен быть зарегистрирован хотя бы один DataProcessor
.
dataRegistry.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
ImmutableHealthData.class, ImmutableSpongeHealthData.class,
new HealthDataProcessor());
ValueProcessors
Процессоры значений зарегистрированы в нижней части той же самой функции. Для каждого «Key» могут быть зарегистрированы несколько процессоров путем последовательных вызовов метода «registerValueProcessor()».
dataRegistry.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
dataRegistry.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. Может быть полезно взглянуть на уже реализованные процессоры, аналогичные тому, над которым вы работаете, чтобы лучше понять, как он должен работать.
Если вы застряли или не уверены в некоторых аспектах, зайдите на канал интернет-ретрансляции «#spongedev», на форумы или откройте вопрос на GitHub. Обязательно проверьте ‘Контрольный список реализации процессора данных <https://github.com/SpongePowered/SpongeCommon/issues/8>_ на предмет соответствия общим требованиям к участию.