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:

  1. Terapkan ' ' datamanipulator ' ' sendiri

  2. Implement the ImmutableDataManipulator

Bila langkah-langkah ini telah selesai, berikut juga harus dilakukan:

  1. Register the Key in the KeyRegistryModule

  2. Terapkan ' ' datamanipulator ' '

  3. Implement the ValueProcessor for each Value being represented by the DataManipulator

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 AbstrakData konstruktor, 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 mendapatkan Nilai yang 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 dengan ImmutableSponge

  • Mewarisi dari ImmutableAbstractData sebagai gantinya

  • Ganti dari registerGettersAndSetters(), metode ini disebut 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.

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.