Menerapkan datamanipulator
Ini merupakan petunjuk bagi para kontributor yang ingin membantu pelaksanaan Data API dengan membuat DataManipulators. Daftar DataManipulators yang telah terupdate yang akan dijalankan bisa di temukan di 'SpongeCommon Issue #8 <https://github.com/SpongePowered/SpongeCommon/issues/8>'_.
To fully implement a DataManipulator these steps must be followed:
Terapkan ' ' datamanipulator ' ' sendiri
Implement the ImmutableDataManipulator
Bila langkah-langkah ini telah selesai, berikut juga harus dilakukan:
Register the Key in the
KeyRegistryModuleTerapkan ' ' datamanipulator ' '
Implement the
ValueProcessorfor each Value being represented by theDataManipulator
Jika data berlaku untuk blok, beberapa metode juga harus dicampur ke blok.
Catatan
Pastikan anda mengukiti ../petunjuk kami.
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. Terapkan 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 {
[...]
}
Ada dua jenis argumen untuk AbstractData kelas. Yang pertama adalah interface yang diimplementasikan oleh kelas ini, yang kedua adalah antarmuka yang dilaksanakan oleh yang bersangkutan, ImmutableDataManipulator.
Pembuatnya
Dalam kebanyakan kasus saat menerapkan abstrak ' ' Datamanipulator ' ' kamu harus memiliki dua konstruktor:
Satu tanpa argumen (tidak-args) yang akan memanggil konstruktor kedua dengan "gagal" nilai
Konstruktor kedua yang mengambil semua nilai yang didukungnya.
Konstruktor kedua harus
lakukan panggilan ke
AbstrakDatakonstruktor, melewati tahapan rujukan untuk pelaksanaan antar muka.pastikan nilai yang dilewatkan benar
hubungi
registerGettersAndSetters()metode
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.
Catatan
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.
Para pengakses ditentukan oleh Antarmuka
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();
}
Tip
Karena Double adalah Comparable, kita tidak perlu secara eksplisit menentukan komparator.
If no current value is specified, calling BaseValue#get() on the Value returns the default value.
Penyalinan dan serialisasi
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()
Metode toContainer() digunakan untuk serialisasi tujuan. Menggunakan MemoryDataContainer sebagai hasil dan menerapkan nilai-nilai yang disimpan dalam contoh ini. DataContainer pada dasarnya adalah sebuah peta pemetaan DataQuerys untuk nilai-nilai. Sejak a Kunci selalu berisi sesuai DataQuery, hanya digunakan oleh orang-orang yang lewat Kunci ini diatas:
register a Supplier to directly get the value
register a Consumer to directly set the value
daftarkan
Supplier<Value>untuk mendapatkanNilaiyang berubah-ubah
Supplier dan Consumer merupakan penghubung fungsional, sehingga Java 8 Lambdas dapat digunakan.
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.
Tip
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.
Itu dia, ' ' Datamanipulator ' ' harus dilakukan sekarang.
2. Terapkan ImmutableDataManipulator
Implementing the ImmutableDataManipulator is similar to implementing the mutable one.
Satu-satunya perbedaan adalah:
Nama kelas dibentuk oleh awalan nama
DataManipulators yang bisa dapat diubah denganImmutableSpongeMewarisi dari
ImmutableAbstractDatasebagai gantinyaGanti dari
registerGettersAndSetters(), metode ini disebutregisterGetters()
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.
Tip
Anda harus mendeklarasikan bagian dari ImmutableDataManipulator sebagai final untuk mencegah perubahan yang tidak disengaja.
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. Terapkan DataProcessors
Berikutnya adalah DataProcessor. DataProcessor berfungsi sebagai jembatan antara kita DataManipulator dan Minecraft benda-benda. Setiap kali ada data yang diminta atau ditawarkan untuk DataHolders yang ada di Minecraft Vanilla, panggilan-panggilan itu berakhir menjadi didelegasikan ke DataProcessor atau ValueProcessor.
For your name, you should use the name of the DataManipulator interface and append Processor. Thus, for
HealthData we create a HealthDataProcessor.
Dalam rangka untuk mengurangi kode boilerplate, DataProcessor harus mewarisi dari yang sesuai kelas abstrak dalam org.spongepowered.umum.data.prosesor.umum paket. Karena kesehatan hanya dapat hadir pada badan-badan tertentu, kita dapat menggunakan AbstractEntityDataProcessor yang khusus ditujukan pada Entitas yang didasarkan pada net.minecraft.entitas.Entitas. AbstractEntitySingleDataProcessor akan memerlukan waktu kurang pelaksanaan pekerjaan, tetapi tidak dapat digunakan sebagai HealthData berisi lebih dari satu nilai.
public class HealthDataProcessor
extends AbstractEntityDataProcessor<EntityLivingBase, HealthData, ImmutableHealthData> {
public HealthDataProcessor() {
super(EntityLivingBase.class);
}
[...]
}
Tergantung pada abstraksi yang anda gunakan, metode yang anda harus menerapkan mungkin sangat berbeda, tergantung pada seberapa jauh pelaksanaan pekerjaan sudah bisa dilakukan di kelas abstrak. Umumnya, metode-metode yang dapat dikategorikan.
Tip
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.
Metode validasi
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.
Untuk HealthDataProcessor kami Support() dijalankan oleh AbstractEntityDataProcessor. Per default, hal tersebut akan kembali benar jika argumen yang diberikan merupakan turunan dari kelas yang telah ditentukan saat memanggil super() konstruktor.
Sebagai gantinya, kita diminta untuk menyediakan metode doesDataExist(). Karena abstraksi tidak tahu bagaimana mendapatkan data, hal tersebut membiarkan fungsi ini dijalankan. Seperti nama nya, metode tersebut harus memeriksa apakah data sudah ada pada target yang didukung. Untuk HealthDataProcessor, hal ini selalu kembali benar, karena setiap entitas yang hidup selalu memiliki kesehatan.
@Override
protected boolean doesDataExist(EntityLivingBase entity) {
return true;
}
Metode Pengaturan
Metode pengaturan menerima DataHolder dari beberapa jenis dan beberapa data yang harus dijalankan padanya, jika memungkinkan.
Antarmuka DataProcessor mendefinisikan sebuah set() metode menerima``DataHolder`` dan DataManipulator yang mengembalikan DataTransactionResult. Bergantung pada kelas abstraksi yang digunakan, beberapa fungsi yang diperlukan mungkin sudah diterapkan.
Pada kasus ini, AbstractEntitydataProcessor mengurus sebagian besar proses dan hanya membutuhkan sebuah metode untuk mengeset sebagian nilai agar kembali true jika metode tersebut sukses dijalankan dan false jika tidak sukses. Semua pengecekan jika DataHolder mendukung Data akan ditangani, kelas abstrak hanya akan memberikan peta tiap Key dari DataManipulator ke nilainya dan kemudian menyusun DataTransactionResult tergantung apakah operasi berhasi dijalankan atau tidak.
@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;
}
Tip
To understand DataTransactionResults, check the corresponding docs page and refer to the DataTransactionResult.Builder docs to create one.
Peringatan
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.
Metode penghapusan
Metode remove() mencoba untuk menghapus data dari DataHolder dan mengembalikan 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();
}
Metode Getter
Metode getter mendapatkan data dari DataHolder dan mengembalikan``DataManipulator`` opsional. Antarmuka DataProcessor menentukan metode dari() dan createFrom(), perbedaannya adalah bahwa dari() akan kembali Opsional.empty() jika dudukan data kompatibel, namun saat ini tidak berisi data, sementara createFrom() akan memberikan DataManipulator memegang nilai bawaan dalam kasus itu.
Sekali lagi, AbstractEntityDataProcessor akan menyediakan sebagian besar pengerjaan untuk prosess ini dan hanya membutuhkan satu metode untuk mendapatkan nilai sebenarnya yang tersedia di DataHolder. Metode ini hanya di butuhkan setelah supports() dan doesDataExist() keduanya telah kembali benar, artinya metode tersebut bekerja dengan anggapan bahwa data tersedia.
Peringatan
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);
}
Metode penyaringan
Metode pengisian berbeda dari metode getter ketika menerima DataManipulator untuk diisi dengan nilai-nilai. Nilai-nilai tersebut bisa datang dari DataHolder atau harus diserialkan kembali dari DataContainer. Metode ini mengembalikan Optional.empty() jika DataHolder tidak sesuai.
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.
Metode lain
Tergantung pada abstract superclass yang digunakan, beberapa metode lain mungkin akan di butuhkan. Sebagai contoh, AbstractentityDataProcessor perlu membuat DataManipulator secara instan di beberapa titik. Hal ini tidak bisa di lakukan karena metode tersebut tidak mengetahui kelompok pelaksana juga tidak mengetahui konstruktor yang akan di gunakan. Oleh sebab itu metode ini akan menggunakan fungsi abstrak yang harus disediakan oleh pelaksana akhir. Hal ini hanya membuat DataManipulator dengan data bawaan.
Jika anda menjalankan DataManipulator anda sesuai dengan yang di rekomendasikan, anda hanya perlu menggunakan no-args konstruktor.
@Override
protected HealthData createManipulator() {
return new SpongeHealthData();
}
5. Terapkan ValueProcessors
Tidak hanya DataManipulator yang mungkin di tawarkan kepada DataHolder, tetapi juga kelengkapan Value dengan sendiri nya. Untuk itu, anda harus menyediakan minimal satu ValueProcessor untuk setiap Key yang ada di DataManipulator anda. Sebuah ValueProcessor dinamai mengikuti nama tetap setiap Key di dalam kelompok Keys dengan bentuk yang sama dengan DataQuery. Nama tetap tersebut dihilangkan garis bawah, digunakan dalam bentuk huruf besar dan di gabungkan dengan ValueProcessor.
ValueProcessor harus selalu di ambil dari AbstractSpongeValueProcessor, yang akan memproses sebagian pengecekan Support() sesuai dengan jenis DataHolder. Untuk Key.HEALTH, kita akan membuat dan mengkonstruk HealthValueProcessor sebagai berikut.
public class HealthValueProcessor
extends AbstractSpongeValueProcessor<EntityLivingBase, Double, MutableBoundedValue<Double>> {
public HealthValueProcessor() {
super(EntityLivingBase.class, Keys.HEALTH);
}
[...]
}
Sekarang AbstractSpongeValueProcessor akan menggantikan kita dari keharusan mengecek apakah nilai telah didukung. Hal ini akan di asumsikan telah didukung jika target ValueContainer adalah jenis EntityLivingBase.
Tip
Untuk lebih memudahkan pengendalian terhadap objek EntityLivingBase yang telah didukung, metode support(EntityLivingBase) bisa diganti.
Sekali lagi, kebanyakan proses telah dilakukan oleh bagian abstraksi. Kita hanya perlu menjalankan dua metode pembantu untuk membuat value dan mitra tetapnya dan tiga metode untuk mendapatkan, mengatur dan menghapus data.
@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());
}
Dikarenakan tidak mungkin sebuah EntityLivingBase tidak memiliki kesehatan, metode ini tidak akan pernah kembali ke 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;
}
Metode set() akan mengembalikan nilai boolean yang akan mengindikasikan apakah nilai dapat berhasil diatur. Melakukan proses ini akan menolak nilai di luar batas-batas yang telah digunakan didalam metode konstruksi nilai kami di atas.
@Override
public DataTransactionResult removeFrom(ValueContainer<?> container) {
return DataTransactionResult.failNoData();
}
Karena data dijamin selalu hadirkan, usaha untuk menghapusnya selalu gagal.
6. Daftar prosesor
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.
Dataprosesor
DataProcessor terdaftar bersama kelas penghubung dan pelaksana dari DataManipulator yang ditanganinya. Untuk setiap DataManipulators yang tidak kekal/kekal minimal satu DataProcessor harus terdaftar.
DataUtil.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
ImmutableHealthData.class, ImmutableSpongeHealthData.class,
new HealthDataProcessor());
Nilaiprosesor
Nilai prosesor terdaftar dibagian bawah dari fungsi yang sama. Untuk setiap Key beberapa prosesor bisa didaftarkan dengan beberapa panggilan dari metode registeryValueProcessor().
DataUtil.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
DataUtil.registerValueProcessor(Keys.MAX_HEALTH, new MaxHealthValueProcessor());
Melakukan data blok
Blok data berbeda-beda dari satu jenis dengan yang lain dimana hal tersebut di jalankan dengan mencampurkan blok tersebut dengan blok itu sendiri. Ada beberapa metode di org.spongepowered.mixin.core.block.MixinBlock yang harus diganti untuk menjalankan data untuk setiap blok.
@Mixin(BlockHorizontal.class)
public abstract class MixinBlockHorizontal extends MixinBlock {
[...]
}
supports() harus kembali benar jika baik penghubung ImmutableDataManipulator bisa dialihkan dari Class yang dilalui sebagai argumen, atau superkelas mendukungnya.
@Override
public boolean supports(Class<? extends ImmutableDataManipulator<?, ?>> immutable) {
return super.supports(immutable) || ImmutableDirectionalData.class.isAssignableFrom(immutable);
}
getStateWithData() harus kembali baru BlockState dengan data dari ImmutableDataManipulator yang dijalankan untuk nya. Jika manipolator tidak didukung secara langsung, metode harus didelegasikan kepada superclass.
@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() sama dengan get StateWithData(), tetapi bekerja dengan satu Keys.
@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);
}
Pada akhirnya, getManipulators() harus mengembalikan dafter semua immutableDataManipulators blok yang didukung, bersamaan dengan nial sekarang untuk disajikan IBlockState. Data tersebut harus mencakup semua ImmutableDataManipulators yang berasal dari superclass.
@Override
public List<ImmutableDataManipulator<?, ?>> getManipulators(IBlockState blockState) {
return ImmutableList.<ImmutableDataManipulator<?, ?>>builder()
.addAll(super.getManipulators(blockState))
.add(new ImmutableSpongeDirectionalData(DirectionResolver.getFor(blockState.getValue(BlockHorizontal.FACING))))
.build();
}
Informasi lebih lanjut
Dengan Data yang merupakan konsep yang sedikit abstrak didalam Sponge, hal tersebut mempersulit untuk memberikan petunjuk umum tentang cara memperoleh data yang dibutuhkan dari kelas-kelas Minecraft itu sendiri. Mungkin akan membantu dengan memperhatikan prosesor yang serupa dengan yang anda kerjakan yang telah dijalankan untuk mendapatkan pemahaman yang lebih baik tentang bagaimana prosesor tersebut mestinya bekerja.
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.