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>`_.

Para implementar completamente un “” DataManipulator”” debe seguir estos pasos:

  1. Poner en práctica la “” DataManipulator”” en sí

  2. Poner en práctica el “” ImmutableDataManipulator””

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

  1. Registre la «Clave» en el «Registro de Claves»

  2. Implemente el «DtaProcessor»

  3. Implemente el «ValueProcessor» para cada valos que esta represantado por el «DataManipulatr»

  4. Registre todo en el “” SpongeSerializationRegistry””

Nota

Asegúrese de seguir nuestras :doc:”… /instrucciones.

1. implementar el DataManipulator

La Convención de nomenclatura para las implementaciones de “” DataManipulator”” es el nombre de la interfaz el prefijo «Sponge». Así que para implementar la interfaz “” HealthData””, creamos una clase llamada “” SpongeHealthData”” en el paquete apropiado. Para la aplicación de la “” DataManipulator”” primero tiene extender una clase abstracta apropiada del paquete de “” org.spongepowered.common.data.manipulator.mutable.common””. Genérico que es “” AbstractData”” pero también hay abstracciones que reducen código repetitivo aún más para algunos casos especiales como “” DataManipulator”” s que sólo contienen un único valor.

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 para implementar un Manipulador abstracto debes tener 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 {

    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();
    }

    ...

}

Ya que sabemos que la salud actual y la salud son valores acotados, necesitamos para asegurarse de que no hay valores fuera de estos límites se pueden pasar. Para lograr esto utilizamos “” precondiciones “” de guayaba de los cuales importamos los métodos requeridos estáticamente.

Nota

Nunca utilizar llamados a valores mágicos (números arbitrarios, booleanos etcetera) en el código. En su lugar, localizar la clase de “” org.spongepowered.common.data.util.DataConstants”” y utiliza una constante de ajuste - o crear uno, si es necesario.

Accesorios definidos por la interfaz

La interfaz que implementamos especifica algunos métodos para tener acceso a objetos de “” valor “”. Para “” HealthData””, son “” health()”” y “” maxHealth()””. Cada llamada que debe generar un nuevo “” valor “”.

public MutableBoundedValue<Double> health() {
    return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
        .minimum(DataConstants.MINIMUM_HEALTH)
        .maximum(this.maximumHealth)
        .defaultValue(this.maximumHealth)
        .actualValue(this.currentHealth)
        .build();
}

Truco

Ya que “” Doble “” es “” Comparable””, no necesitamos especificar explícitamente un comparador.

Si no se especifica ningún valor, llamada “” get()”” sobre el “” valor “” devolverá el valor predeterminado.

Copiado y Serializacion

Los dos métodos “” copy()”” y “” asImmutable()”” no son mucho trabajo para poner en práctica. Por tanto usted sólo necesita devolver un manipulador de datos mutable o un manipulador respectivamente, que contenga los mismos datos que la instancia actual.

El metodo toContainer() es usado para propósitos de esterilización. Usa un MemoryDataContainer``como resultado y aplicale los valores almacenados en esta instancia. Un ``DataContainer` es basicamente un mapa de asignacion ``DataQuerys de valores. Ya que Key` siempre contiene un DataQuery correspondiente, usalos pasando la Key directamente.

public DataContainer toContainer() {
    return new MemoryDataContainer()
        .set(Keys.HEALTH, this.currentHealth)
        .set(Keys.MAX_HEALTH, this.maximumHealth);
}

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:

  • registra un “” proveedor “” para obtener directamente el valor

  • registra un “” consumidor “” para establecer directamente el valor

  • registra un “” proveedor <Value>”” para obtener el “” valor “” mutable

“” Proveedor “” y “” consumidor “” son interfaces funcionales, por lo que puede utilizar Java 8 Lambdas.

private void setCurrentHealthIfValid(double value) {
    if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
        this.currentHealth = value;
    } else {
        throw new IllegalArgumentException("Invalid value for current health");
    }
}

private void setMaximumHealthIfValid(double value) {
    if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
        this.maximumHealth = value;
    } else {
        throw new IllegalArgumentException("Invalid value for maximum health");
    }

}

private void registerGettersAndSetters() {
    registerFieldGetter(Keys.HEALTH, () -> SpongeHealthData.this.currentHealth);
    registerFieldSetter(Keys.HEALTH, SpongeHealthData.this::setCurrentHealthIfValid);
    registerKeyValue(Keys.HEALTH, SpongeHealthData.this::health);

    registerFieldGetter(Keys.MAX_HEALTH, () -> SpongeHealthData.this.maximumHealth);
    registerFieldSetter(Keys.MAX_HEALTH, SpongeHealthData.this::setMaximumHealthIfValid);
    registerKeyValue(Keys.MAX_HEALTH, SpongeHealthData.this::maxHealth);
}

El Consumer registrado como organismo de campo debe realizar los controles adecuados para asegurar que el valor proporcionado es válido. Esto se aplica especialmente para “” DataHolder”” s que no acepta valores negativos. Si un valor es válido, debe ser lanzado un “” IllegalArgumentException””.

Truco

Los criterios de validez para los setters son las mismas el respectivo objeto de “” valor “”, por lo que puede delegar la comprobación de validez a un llamado de “” this.health().set()”” y acaba de establecer “” this.currentHealth = valor “ si la primera línea tiene no produce una excepción sin embargo.

Eso es todo. El “” DataManipulator”” debería estar hecho ahora.

2. implementar la ImmutableDataManipulator

“” ImmutableDataManipulator”” la implementación es similar a la aplicación de la mutable.

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()””

Al crear ImmutableDataHolders o ImmutableValues, compruebe si tiene sentido usar el “” ImmutableDataCachingUtil””. Por ejemplo si tienes “” WetData””, que contiene nada más que un valor booleano, es más factible retener sólo dos instancias caché de “” ImmutableWetData”” - uno para cada valor posible. Manipuladores y valores con muchos valores posibles (como “” SignData””) sin embargo, almacenamiento en caché es demostrado para ser demasiado caro.

Truco

Deberías declarar los campos de un ImmutableDataManipulator``como ``final en orden de prevenir cambios accidentales.

3. Registre la clave en el KeyRegistry

El siguiente paso es registrar su Keys to the KeyRegistry. Para ello, localice la clase de “” org.spongepowered.common.data.key.KeyRegistry”” y encuentre la función estática “” generateKeyMap()””. Ahí agregue una línea para registrarse (y crear) las llaves usadas.

keyMap.put("health"), makeSingleKey(Double.class, MutableBoundedValue.class, of("Health")));
keyMap.put("max_health", makeSingleKey(Double.class, MutableBoundedValue.class, of("MaxHealth")));

El keyMap localiza cadenas a “” clave “” s. La cadena que se utiliza debe ser el nombre de la constante correspondiente de la clase de utilidad de las “” llaves “” en minúsculas. La “” llave “” es creada por uno de los métodos estáticos proporcionados por “” KeyFactory””, en la mayoría de los casos “” makeSingleKey””. “” makeSingleKey”” requiere primero una clase de referencia para los datos subyacentes, que en nuestro caso es un «doble», entonces una referencia de clase para el tipo de “” valor “” utilizado. El tercer argumento es el “” DataQuery”” que se usa para la serialización. Se crea el método de “” DataQuery.of()”” importado estáticamente aceptando una cadena. Esta cadena debe ser también el nombre de constante, despojado de subrayados y capitalización cambiada a superior caso de camello.

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.

Para tu nombre, deberías usar el nombre de la interfaz DataManipulator y anexar Processor. Mientras que para HealthData creamos un 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

Es posible crear múltiples DataProcessor s para los mismos datos. Si deben ser soportados DataHolders muy diferentes (por ejemplo tanto una``TileEntity`` como un ItemStack correspondiente), puede ser beneficioso crear un procesador para cada tipo de DataHolder para hacer pleno uso de las abstracciones proporcionadas. Asegúrese de seguir la estructura del paquete para los elementos, entidades de mosaico y entidades.

Métodos de Validación

Siempre devuelve un valor booleano. Si el método es llamado supports() debería realizar un control general de si el objetivo suministrado soporta los tipos de datos manejados por el DataProcessor.

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.

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.

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;
}

Truco

Para entender DataTransactionResult s, mira las paginas correspondientes de los documentos y dirigete a los documentos DataTransactionResult.Builder para crear uno.

Advertencia

Sobre todo cuando se trabaja con “” ItemStack”” s es probable que usted tenga que lidiar con “” NBTTagCompound”” s directamente. Muchas claves NBT ya se definen como constantes en la clase de “” org.spongepowered.common.data.util.NbtDataUtil””. Si la tecla correspondiente no existe, necesita agregar para evitar valores de magia en el código.

Método de Eliminación

El método remove() intenta remover los datos de DataHolder y devuelve un DataTransactionResult.

El método de eliminación no se abstrae en ningún resumen DataProcessor ya que las abstracciones no tienen forma de saber si los datos están siempre presentes en un DataHolder (como WetData o HealthData) compatible o si debe o no estar presente (como LoreData). Si los datos están siempre presentes, remove() siempre fallará, si puede o no que este presente, remove() debería removerlo. En dichos casos el método doesDataExist() deberia ser sobrescrito.

Ya que una entidad viva siempre tiene salud, HealthData siempre es presentada y por lo tanto la eliminación no es soportada. Por lo tanto solo devolvemos failNoData() y no se sobrescribe el método doesDataExist().

public DataTransactionResult remove(DataHolder dataHolder) {
    return DataTransactionBuilder.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

Si los datos puede que no existan siempre en el objetivo``DataHolder``, por ejemplo si la función remove() puede ser exitosa (ver mas arriba), es imperativo que sobrescribas el método doesDataExist() de modo que devuelva true si los datos están presentes y false si no.

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);
}

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 ya maneja el llenado desde DataHolders creando un DataManipulator desde el titular y luego fusionandolo con el manipulador suministrado, pero no puede proporcionar la des-serialización del DataContainer.

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();
}

El método fill() es para devolver un Optional de los Datos de salud alterados, si y solo si toda los datos requeridos pueden ser obtenidos del 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.

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.

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());
}

Ya que es imposible para un EntityLivingBase` no tener salud, este método nunca devolverá ``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;
}

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.

public DataTransactionResult removeFrom(ValueContainer<?> container) {
    return DataTransactionBuilder.failNoData();
}

Ya que es garantizado que los datos estarán siempre presentes, cualquier intento de removerlos fallará.

6. Procesadores de Registro

Para que Sponge pueda usar nuestros manipuladores y procesadores, necesitamos registrarlos. Esto es hecho en la clase org.spongepowered.common.data.SpongeSerializationRegistry. En el método setupSerialization hay dos bloques mas grandes de registro a los que añadir nuestros procesadores.

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.

dataRegistry.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().

dataRegistry.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
dataRegistry.registerValueProcessor(Keys.MAX_HEALTH, new MaxHealthValueProcessor());

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.

Si está atascado o no está seguro de ciertos aspectos, visite el canal IRC de #spongedev, los foros, o abra un problema en GitHub. Asegure de comprobar la Lista de comprobación de implementación del procesador de datos para los requisitos generales de contribución.