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:
Poner en práctica la “” DataManipulator”” en sí
Poner en práctica el “” ImmutableDataManipulator””
Cuando haya completado estos pasos, también debe hacer lo siguiente:
Registre la «Clave» en el «Registro de Claves»
Implemente el «DtaProcessor»
Implemente el «ValueProcessor» para cada valos que esta represantado por el «DataManipulatr»
Registre todo en el “” SpongeSerializationRegistry””
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.
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 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 {
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 ``DataQuery
s 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
DataManipulator
s conImmutableSponge
Heredar de “” ImmutableAbstractData”” en vez de
En lugar de “” registerGettersAndSetters()””, el método se llama “” registerGetters()””
Al crear ImmutableDataHolder
s o ImmutableValue
s, 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 Key
s 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 DataHolder
s 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 DataManipulator
s 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());
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.
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.