Implementando DataManipulators
Esta es una guía para los colaboradores que quieran ayudar con la implementación de la API de datos mediante la creación de DataManipulators. Puede encontrarse una lista actualizada de DataManipulators a aplicarse en el problema SpongeCommon #8 <https://github.com/SpongePowered/SpongeCommon/issues/8>`_.
To fully implement a DataManipulator these steps must be followed:
Poner en práctica la “” DataManipulator”” en sí
Implement the ImmutableDataManipulator
Cuando haya completado estos pasos, también debe hacer lo siguiente:
Register the Key in the
KeyRegistryModule
Implemente el «DtaProcessor»
Implement the
ValueProcessor
for each Value being represented by theDataManipulator
Si los datos se aplican a un bloque, varios métodos deben también ser mezclados al bloque.
Nota
Asegúrese de seguir nuestras :doc:”… /instrucciones.
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. implementar el 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 {
[...]
}
Hay dos argumentos de tipo para la clase de AbstractData. La primera es la interfaz implementada por esta clase, la segunda es la interfaz implementada por la correspondiente “”ImmutableDataManipulator””.
El Constructor
En la mayoría de los casos mientras implementaba un resumen “” DataManipulator”” necesita de dos constructores:
Uno sin argumentos (no-args) que llama al segundo constructor con valores «por defecto»
El segundo constructor toma todos los valores que soporta.
El segundo constructor debe
realizar una llamada al constructor de “” AbstractData””, pasando por la referencia de clase para la interfaz implementada.
asegúrese de que los valores pasados sean válidos
llame al método de “” 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.
Nota
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.
Accesorios definidos por la interfaz
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();
}
Truco
Ya que “” Doble “” es “” Comparable””, no necesitamos especificar explícitamente un comparador.
If no current value is specified, calling BaseValue#get() on the Value
returns the default value.
Copiado y Serializacion
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()
Un “”DataManipulator”” también proporciona métodos para obtener y establecer datos mediante teclas. La implementación de esto se maneja por “” AbstractData””, pero hay que decirlo que puede acceder a los datos y cómo. Por lo tanto, en el método de “” registerGettersAndSetters()”” tenemos que hacer lo siguiente para cada valor:
register a Supplier to directly get the value
register a Consumer to directly set the value
registra un “” proveedor <Value>”” para obtener el “” valor “” mutable
“” Proveedor “” y “” consumidor “” son interfaces funcionales, por lo que puede utilizar Java 8 Lambdas.
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.
Truco
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.
Eso es todo. El “” DataManipulator”” debería estar hecho ahora.
2. implementar la ImmutableDataManipulator
Implementing the ImmutableDataManipulator is similar to implementing the mutable one.
Las únicas diferencias son:
El nombre de clase es formado prefijando el nombre mutable de
DataManipulator
s conImmutableSponge
Heredar de “” ImmutableAbstractData”” en vez de
En lugar de “” registerGettersAndSetters()””, el método se llama “” 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.
Truco
Deberías declarar los campos de un ImmutableDataManipulator``como ``final
en orden de prevenir cambios accidentales.
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. Implementar los DataProcessors
Lo siguiente son los DataProcessor
. Un``DataProcessor`` funciona como un fuente entre los objetos de nuestro DataManipulator
y los de Minecraft’s. Cuando cualquier dato sea solicitado desde u ofrecido a DataHolders
que existen en Vanilla Minecraft, estas llamadas podrian terminar siendo delegadas a DataProcessor
o a ValueProcessor
.
For your name, you should use the name of the DataManipulator
interface and append Processor
. Thus, for
HealthData
we create a HealthDataProcessor
.
En orden de reducir el código repetitivo,el DataProcessor
debe heredar de la clase abstracta apropiada en el paquete org.spongepowered.common.data.processor.common
. Puesto que la salud sólo puede estar presente en determinadas entidades, podemos hacer uso de la AbstractEntityDataProcessor` que está destinada específicamente a ``Entities
basadas en net.minecraft.entity.Entity`. ``AbstractEntitySingleDataProcessor
” requiere menos trabajo de aplicación, pero no puede ser utilizado ya que HealthData
contiene más de un valor.
public class HealthDataProcessor
extends AbstractEntityDataProcessor<EntityLivingBase, HealthData, ImmutableHealthData> {
public HealthDataProcessor() {
super(EntityLivingBase.class);
}
[...]
}
Dependiendo de que abstracción uses, los métodos que tienes que implementar pueden diferir bastante, dependiendo de cuánto trabajo de implementación ya se haya podido hacer en la clase abstracta. Generalmente, los métodos pueden ser categorizados.
Truco
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.
Métodos de Validación
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.
Nuestro``HealthDataProcessor`` supports()
es implementado por la AbstractEntityDataProcessor
. Por defecto, devolverá true si el argumento suministrado es una instancia de la clase especificada al llamar al constructor super()
.
Por el contrario, requerimos a proporcionar un método doesDataExist()
. Puesto que la abstracción no sabe cómo obtener los datos, deja esta función a implementar. Como el nombre lo indica, el método debe comprobar si el dato ya existe en el destino soportado. Para el HealthDataProcessor
, esto siempre devuelve true, porque cada entidad viva siempre tiene salud.
@Override
protected boolean doesDataExist(EntityLivingBase entity) {
return true;
}
Metodos Setter
Un método setter recibe un DataHolder
de algún tipo y algunos datos que deberían ser aplicados a el, si es posible.
La interfaz DataProcessor
define un método set()
aceptando un DataHolder
y un DataManipulator
que devuelve un DataTransactionResult
. Dependiendo de la clase de abstracción utilizada, algunas de las funciones necesarias ya podrían ser implementadas.
En este caso, el AbstractEntityDataProcessor
se encarga de la mayor parte de eso y sólo requiere de un método para establecer algunos valores para devolver true
si fue exitoso y false
si no. Todo comprueba si el DataHolder
soporta la Data
de la que esta a cargo, la clase abstracta sólo pasará un Mapa revisando cada Key
desde el DataManipulator
hasta su valor y luego construirá un DataTransactionResult
dependiendo de si la operación fue acertada o no.
@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;
}
Truco
To understand DataTransactionResults, check the corresponding docs page and refer to the DataTransactionResult.Builder docs to create one.
Advertencia
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.
Método de Eliminación
El método remove()
intenta remover los datos de DataHolder
y devuelve un 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();
}
Metodos Getter
Los métodos getter obtienen los datos de un DataHolder
y devuelven un DataManipulator
opcional. La interfaz DataProcessor` especifica los métodos ``from()
y createFrom()
, siendo la diferencia que from()
devolverá Optional.empty()
si el titular de datos es compatible, pero de hecho no contiene los datos, mientras createFrom()
proveerá un titular DataManipulator
con valores por defecto en ese caso.
De nuevo, AbstractEntityDataProcessor
proveerá la mayoría de la implementación para este y solo requiere un método para obtener el valor actual presente en el DataHolder
. Este método solo es llamado por supports()
y``doesDataExist()`` ambos devolviendo verdadero, lo que significa que es ejecutado asumiendo que los datos están presentes.
Advertencia
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);
}
Métodos Filler
Un método filler es diferente de un metodo getter en que este recibe un DataManipulator
para llenar con valores. Estos valores o bien vienen de un DataHolder
o tienen que ser des-serializados desde un DataContainer
. El metodo devuelve Optional.empty()
si el DataHolder
es 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
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
.
Otros Metodos
Dependiendo de la superclase abstracta utilizada, otros métodos pueden ser requeridos. Por ejemplo, AbstractEntityDataProcessor
necesita crear instancias DataManipulator
en varios puntos. No puede hacer esto ya que no conoce a la implementación de la clase ni el constructor a utilizar. Por lo tanto utiliza una función abstracta que debe ser proporcionada por la aplicación final. No hace nada más que crear un DataManipulator
con datos por defecto.
Si implementaste tu DataManipulator
como se recomendó, puedes simplemente usar los constructores sin argumentos.
@Override
protected HealthData createManipulator() {
return new SpongeHealthData();
}
5. Implemente los ValueProcessors
Puede ofrecer no sólo un DataManipulator
a un DataHolder
, pero también un Value
propio. Por lo tanto, debe proporcionar al menos un ValueProcessor
para cada Key
en su DataManipulator
. Un ValueProcessor
es nombrado despues de que el nombre de la constante de su Key
en la clase Keys
esté en una manera similar a su DataQuery
. El nombre de la constante es despojado de subrayados “” ValueProcessor””. El nombre de constante es despojado de subrayados, utilizado en mayuscula y luego se le añade el sufijo ValueProcessor
.
Un ValueProcessor
siempre debe heredar de AbstractSpongeValueProcessor
, que ya maneja una porción de los controles supports()
en función del tipo del DataHolder
. Para Keys.HEALTH
, crearemos y construiremos un HealthValueProcessor
como sigue.
public class HealthValueProcessor
extends AbstractSpongeValueProcessor<EntityLivingBase, Double, MutableBoundedValue<Double>> {
public HealthValueProcessor() {
super(EntityLivingBase.class, Keys.HEALTH);
}
[...]
}
Ahora los AbstractSpongeValueProcessor
nos liberaran de la necesidad de comprobar si el valor es soportado. Se asume que es soportado si el objetivo ValueContainer
es del tipo EntityLivingBase
.
Truco
Para un mayor control sobre que objetos EntityLivingBase
son soportados, el método supports(EntityLivingBase)
puede ser sobrescrito.
De nuevo, la mayoría del trabajo es hecho por la clase abstracción. Solo necesitamos implementar dos métodos de ayuda para crear un Value
y sus contrapartes inmutables y tres métodos para obtener, configura y establece los datos.
@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());
}
Ya que es imposible para un EntityLivingBase` no tener salud, este método nunca devolverá ``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;
}
El método set()
devolverá un valor booleano indicando si el valor podría ser establecido exitosamente. Esta implementación rechazará los valores por fuera de los limites usados en nuestros métodos de construcción de valor anteriormente.
@Override
public DataTransactionResult removeFrom(ValueContainer<?> container) {
return DataTransactionResult.failNoData();
}
Ya que es garantizado que los datos estarán siempre presentes, cualquier intento de removerlos fallará.
6. Procesadores de Registro
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.
ProcesadoresDeDatos
Un DataProcessor
es registrado junto con la interfaz y las clases de implementación del DataManipulator
que maneja. Para cada par de DataManipulator
s mutable / inmutable al menos un DataProcessor
debe ser registrado.
DataUtil.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
ImmutableHealthData.class, ImmutableSpongeHealthData.class,
new HealthDataProcessor());
ProcesadoresDeValor
Los procesadores de valor son registrados al fondo de la propia función, Para cada Key
pueden ser registrados múltiples procesadores por llamadas subsecuentes del método registerValueProcessor()
.
DataUtil.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
DataUtil.registerValueProcessor(Keys.MAX_HEALTH, new MaxHealthValueProcessor());
Implementando Datos de Bloques
Los datos de bloques son un poco diferentes de otros tipos de datos que son implementados mezclándose en los bloques como tal. Hay varios métodos en org.spongepowered.mixin.core.block.MixinBlock
que deben ser sobrescritos para implementar los datos para los bloques.
@Mixin(BlockHorizontal.class)
public abstract class MixinBlockHorizontal extends MixinBlock {
[...]
}
supports()
debería devolver true
si bien la interfaz ImmutableDataManipulator
es asignable desde la Class
pasada como argumento, o si la superclase la soporta.
@Override
public boolean supports(Class<? extends ImmutableDataManipulator<?, ?>> immutable) {
return super.supports(immutable) || ImmutableDirectionalData.class.isAssignableFrom(immutable);
}
getStateWithData()
deberia devolver un nuevo BlockState
con los datos del ``ImmutableDataManipulator` aplicados a él. Si el manipulador no es soportado directamente, el método debería delegar a la superclase.
@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()
es el equivalente de``getStateWithData()``, pero funciona con Key
s individuales.
@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);
}
Por último, getManipulators()
deberia devolver una lista de los “” ImmutableDataManipulator”” s soporta el bloque, junto con los valores actuales para el “” IBlockState”” siempre. Debe incluir todos “” ImmutableDataManipulator”” s de la superclase.
@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();
}
Mas Información
Con Data
siendo un concepto algo abstracto en la esponja, es difícil dar instrucciones generales sobre cómo adquirir los datos necesarios de las clases de Minecraft sí mismo. Puede ser útil echar un vistazo a procesadores ya puesto en ejecución similares a la que está trabajando para obtener una mejor comprensión de cómo debería funcionar.
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.