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:

  1. Poner en práctica la “” DataManipulator”” en sí
  2. Implement the ImmutableDataManipulator

Cuando haya completado estos pasos, también debe hacer lo siguiente:

  1. Register the Key in the KeyRegistryModule
  2. Implemente el «DtaProcessor»
  3. Implement the ValueProcessor for each Value being represented by the DataManipulator

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 DataManipulators 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 DataManipulators con ImmutableSponge
  • Heredar de “” ImmutableAbstractData”” en vez de
  • En lugar de “” registerGettersAndSetters()””, el método se llama “” registerGetters()””

When creating ImmutableDataHolders or ImmutableValues, 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 Keys 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 DataProcessors for the same data. If vastly different DataHolders should be supported (for example both a TileEntity and a matching ItemStack), it may be beneficial to create one processor for each type of DataHolder in order to make full use of the provided abstractions. Make sure you follow the package structure for items, tileentities and entities.

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 NBTTagCompounds 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 DataManipulators 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 Keys 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.