Implementierung des DataManipulators

Warnung

These docs were written for SpongeAPI 7 and are likely out of date. If you feel like you can help update them, please submit a PR!

Dies ist eine Einführung für Mitwirkende, die bei der Implementierung der Data API helfen wollen, indem sie DataManipulatoren erstellen. Eine aktuelle Liste der DataManipulatoren, die noch implementiert werden müssen, ist bei SpongeCommon Issue #8 zu finden.

Um einen DataManipulator vollständig zu implementieren, sollte diesen Schritten gefolgt werden:

  1. Den DataManipulator selbst implementieren

  2. Den ImmutableDataManipulator implementieren

Wenn diese Schritte abgeschlossen sind, muss folgendes noch getan werden:

  1. Registrierung des Keys in der KeyRegistryModule

  2. Implementierung des DataProcessor

  3. Den ValueProcessor für jede Value implementieren, den der DataManipulator repräsentiert

Wenn sich die Daten auf einen Block beziehen, musst du auch einige Methoden in den Block mixen.

Bemerkung

Achte darauf, dass du den Richtlinien für Beiträge folgst.

Der folgende Codeausschnitt zeigt die Importe/Pfade für einige Klassen in SpongeCommon, die du ggf. brauchst:

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. Implementierung des DataManipulators

Die Namens Konvention für DataManipulator Implementierungen sieht vor, dass dem Namen des Interfaces „Sponge“ vorangestellt wird. Also um das HealthData interface zu implementieren, würden wir ein Interface namens SpongeHealthData im entsprechenden package erstellen. Für die Implementierung des DataManipulator müssen wir von einer entsprechenden abstrakten Klasse aus dem org.spongepowered.common.data.manipulator.mutable.common Package erben. Die generischste vorhandene Implementierung ist die AbstractData, aber es gibt auch einige Abstraktionen für besondere Fälle, wie DataManipulatoren, die nur einen Wert enthalten. Diese reduzieren die Menge des zu schreibenden Codes deutlich.

public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {
    [...]
}

Es gibt zwei Argumente für die AbstractData Klasse. Das Erste ist das Interface, das von dieser Klasse implementieren werden soll, das Zweite das Interface, das durch den passenden ImmutableDataManipulator implementiert wird.

Der Konstruktor

Wenn du den abstrakten DataManipulator implementierst, wirst du in den meisten Fällen zwei Konstruktoren benötigen:

  • Einen ohne Argumente (no-args), der einen zweiten Konstruktor mit „Standardwerten“ aufruft

  • Der zweite Konstruktor, der alle Werte annimmt, die er unterstützt.

Der zweite Konstruktor muss

  • mache einen Aufruf zum AbstractData Konstruktor und übergebe die Klassenreferenz des implementierten Interfaces.

  • stell sicher, dass die übergebenen Werte gültig sind

  • ruf die Methode registerGettersAndSetters() auf

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.

Bemerkung

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.

Durch das Interface definierte Accessoren

Die Schnittstelle, die wir implementieren gibt einige Methoden, um Value Objekte zugreifen. Bei HealthData sind diese HealthData#health() und HealthData#maxHealth(). Jeder Aufruf von diesen sollte einen neuen Value ergeben.

public MutableBoundedValue<Double> health() {
    return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
        .minimum(0)
        .maximum(this.maxHealth)
        .defaultValue(this.maxHealth)
        .actualValue(this.health)
        .build();
}

Tipp

Da Double ein Comparable ist, müssen wir nicht extra einen Komparator spezifizieren.

Wenn kein aktueller Wert angegeben ist, gibt das aufrufen von BaseValue#get() den Standardwert zurück.

Kopieren und Serialisierung

Die zwei Methoden DataManipulator#copy() und DataManipulator#asImmutable() sind einfach zu implementieren. Für beide musst du nur einen veränderbaren bzw. unveränderbaren Datamanipulator zurückgeben, der die gleichen Werte enthält wie die aktuelle Instanz.

Die Methode DataSerializable#toContainer() wird während der Serialisierung verwendet. Verwende DataContainer#createNew() als Ergebnis und füge diesem die Werte der aktuellen Instanz hinzu. Ein DataContainer ist im Grunde ein Wörterbuch, indem die zu der DataQuery passenden Werte gespeichert sind. Da ein Key (Schlüssel) immer auch eine dazugehörige DataQuery enthält, solltest du einfach direkt den passenden Key verwenden.

public DataContainer toContainer() {
    return super.toContainer()
        .set(Keys.HEALTH, this.health)
        .set(Keys.MAX_HEALTH, this.maxHealth);
}

registerGettersAndSetters()

Ein DataManipulator bietet auch Methoden zum Auslesen und Setzen von Daten mit Hilfe von Keys. Die Implementierung hierfür wird von AbstractData gehandhabt, allerdings müssen wir dieser sagen, auf welche Daten zugegriffen werden kann und wie. Deshalb müssen wir in in der registerGettersAndSetters() Methode folgendes für jeden Wert tun:

  • registriere einen Supplier um direkt den Wert zu erhalten

  • registriere einen Consumer um den Wert direkt setzen zu können

  • registriere einen Supplier<Value> um einen veränderbaren Value zu holen

Supplier und Consumer sind funktionale Interfaces, also kann man hierfür Java 8 Lambdas verwenden.

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

Der als Feldsetzer verwendete Consumer muss die passenden Überprüfungen durchführen, um sicherzustellen, dass der übergebene Wert auch gültig ist. Dies trifft insbesondere auf DataHolder zu, die keine negativen Werte erlauben. Falls ein Wert ungültig ist, sollte eine IllegalArgumentException geworfen werden.

Tipp

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.

Das wars. Der DataManipulator sollte nur fertig sein.

2. Den ImmutableDataManipulator implementieren

Die Implementierung des (unveränderbaren) ImmutableDataManipulator ist ähnlich zu dem des Veränderbaren.

Die einzigen Unterschiede sind:

  • Der Klassenname wird gebildet durch das Voranstellen von ImmutableSponge vor den Namen des veränderlichen DataManipulators

  • Leiten sie stattdessen von ImmutableAbstractData ab

  • Anstatt von registerGettersAndSetters() heißt die Methode 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.

Tipp

Du solltest Felder von ImmutableDataManipulator als final deklarieren um unbeabsichtigte Änderungen zu vermeiden.

3. Registriere den Key in der KeyRegistryModule

Der nächste Schritt ist es die Keys in Keys zu registrieren. Hierzu suchen Sie die KeyRegistryModule-Klasse und finden Sie die registerDefaults()-Methode zu. Dort fügen Sie eine Zeile hinzu um Ihre gebrauchten Schlüssel registrieren (und erstellen).

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

Die register(Key)`\-Methode registriert deine ``Keys, damit du sie später verwenden kannst. Der String, den du als Id verwendest, sollte dem Namen der Konstante in der Keys-Klasse in Kleinbuchstaben entsprechen. Der Key selbst wird durch einen Key.Builder erstellt, der von Key#builder() bereitgestellt wird. Du musst dort ein TypeToken, eine id, einen für Menschen lesbaren namen und eine DataQuery definieren. Die DataQuery wird für die Serialisierung verwendet und kann mit Hilfe der statisch importierten DataQuery.of()-Methode erstellt werden, die wiederum einen String benötigt. Dieser String sollte ebenfalls dem Konstantennamen entsprechen, allerdings ohne Unterstriche und nur unter Verwendung von Camel-Case beginnend mit einem Großbuchstaben.

4. Implementierung des DataProcessors

Als nächstes kommt der DataProcessor. Ein DataProcessor dient als Brücke zwischen unserem DataManipulator und den Objekten von Minecraft. Wann immer irgendwelche Daten von einen, in Vanilla Minecraft existierendem, DataHolders angefordert oder diesem angeboten werden, werden die Aufrufe an einen DataProcessor oder ValueProcessor weitergeleitet.

For your name, you should use the name of the DataManipulator interface and append Processor. Thus, for HealthData we create a HealthDataProcessor.

Um unnötigen Code zu reduzieren, sollte der DataProcessor von der entsprechenden abstrakten Klasse im org.spongepowered.common.data.processor.common erben. Da es Gesundheit nur bei bestimmten Entities gibt, können wir Gebrauch vom AbstractEntityDataProcessor machen, der speziell auf Entities auf der Basis von net.minecraft.entity.Entity ausgerichtet ist. AbstractEntitySingleDataProcessor würde weniger Implementationsarbeit benötigen, kann aber nicht benutzt werden, da HealthData mehr als nur einen Wert enthält.

public class HealthDataProcessor
        extends AbstractEntityDataProcessor<EntityLivingBase, HealthData, ImmutableHealthData> {

    public HealthDataProcessor() {
        super(EntityLivingBase.class);
    }

    [...]

}

Je nachdem, welche Abstraktion du verwendest, können sich die Methoden, die du implementieren musst, stark unterscheiden, abhängig davon, wie viel schon in der abstrakten Klasse implementiert werden konnte. Im Allgemeinen können die Methoden kategorisiert werden.

Tipp

Es ist möglich, mehrere DataProcessoren für die selben Daten zu erstellen. Wenn sehr unterschiedliche DataHolder unterstützt werden sollen (zum Beispiel ein TileEntity und ein passender ItemStack), könnte es vorteilhaft sein, für jede Art von DataHolder einen Prozessor zu erstellen, um um die bereitgestellten Abstraktionen vollständig nutzen zu können. Stelle sicher, dass du die Package-Struktur für Items, Tileentities und Entities nutzt.

Validierungsmethoden

Gebe immer einen Boolean Wert zurück. Wenn eine der supports(target)-Methoden aufgerufen wird, sollte diese einen allgemeinen Check durchführen, ob das übergebene Ziel normalerweise die Art von Daten unterstützt, die unser DataProcessor behandelt. Basierend auf dem Level der Abstraktion musst du die Methode möglicherweise überhaupt nicht implementieren oder wenn dann nur die spezifischste Variante, da die generischeren Varianten dorthin weiter delegieren.

Für unseren HealthDataProcessor wird supports() vom AbstractEntityDataProcessor implementiert. Als Standard wird true, wenn das angegebene Argument eine Instanz der Klasse angeben wird, die angegeben wird, wenn der super()-Konstruktor aufgerufen wird, zurückgegeben.

Stattdessen sind wir dazu verpflichtet, eine doesDataExist()-Methode zur Verfügung zu stellen. Da die Abstraktion nicht weiß, wie sie die Daten überwachen soll, lässt die diese Funktion implementiert werden. Wie der Name sagt, soll die Methode überprüfen, ob die Daten beim angegebenen Ziel schon existieren. Für den HealthDataProcessor gibt das immer true zurück, da jedes lebende Entity immer Gesundheit hat.

@Override
protected boolean doesDataExist(EntityLivingBase entity) {
    return true;
}

Setter-Methoden

Eine Setter-Methode erhält einen DataHolder einer Art und einige Daten, die, wenn möglich, angewandt werden sollen.

Das DataProcessor-Interface definiert eine set()-Methode, die DataHolder und DataManipulator annimmt und ein DataTransactionResult zurückgibt. Abhängig von der Abstraktions-Klasse, die benutzt wird, könnten einiger erforderliche Funktionen bereits implementiert sein.

In diesem Fall kümmert sich der AbstractEntityDataProcessor um das Meiste und benötigt nur eine Methode, um einige Rückgabewerte auf true zu setzen, wenn die Aktion erfolgreich war, oder auf false, wenn nicht. Um alle Überprüfungen, ob der DataHolder die Data unterstützt, wird sich gekümmert, die abstrakte Klasse wird einfach eine Zuweisungsliste übergeben, die jedem Key vom DataManipulator ihrem Wert zuweist, und dann ein DataTransactionResult konstruieren, je nachdem, ob die Operation erfolgreich war, oder nicht.

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

Tipp

Um DataTransactionResults zu verstehen, schau dir die entsprechenenden Doku Seiten an und nutze DataTransactionResult.Builder um welche zu erstellen.

Warnung

Vor allem beim Arbeiten mit ItemStacks ist es wahrscheinlich, dass du direkt mit NBTTagCompound``s umgehen musst. Viele NBT-Tags sind schon als konstante Variablen in der ``NbtDataUtil-Klasse definiert. Wenn dein benötigter Key nicht vorhanden ist, musst du ihn hinzufügen, um ‚magische Werte‘ im Code zu vermeiden.

Die Methode zum Entfernen

Die remove()-Methode versucht, Daten aus dem DataHolder zu entfernen und gibt ein DataTransactionResult zurück.

Die remove()-Methode ist nicht in jedem abstrakten DataProcessor abstrahiert, da die Abstraktionen keine Möglichkeit haben, zu wissen, ob die Daten immer auf einem kompatiblen DataHolder (wie WetData oder HealthData) vorhanden sind oder ob sie möglicherweise vorhanden oder nicht vorhanden sind (wie LoreData). Wenn die Daten immer vorhanden sind, muss remove() immer fehlschlagen. Wenn die Daten entweder vorhanden oder nicht vorhanden sein müssen, sollte remove() sie entfernen können.

Da ein lebendes Entity immer Gesundheit hat, gibt es immer HealthData und das Entfernen daher nicht unterstützt. Deshalb geben wir einfach DataTransactionResult#failNoData() zurück.

@Override
public DataTransactionResult remove(DataHolder dataHolder) {
    return DataTransactionResult.failNoData();
}

Getter-Methoden

Getter-Methoden erhalten Daten von einem DataHolder und geben einen optionalen DataManipulator zurück. Die DataProcessor Schnittstelle spezifiziert die Methoden from() und createFrom(), der Unterschied ist, dass from() Optional.empty() zurückgeben wird, wenn der DataHolder kompatibel ist, aber aktuell keine Daten enthält, während createFrom() einen DataManipulator zur Verfügung stellt, der in diesem Fall Default-Werte enthält.

Wieder einmal wird der AbstractEntityDataProcessor die meiste Implementation dafür zur Verfügung stellen und er benötigt nur eine Methode, um die aktuellen Werte auf dem DataHolder zu bekommen. Diese Methode wird nur aufgerufen, nachdem supports() und doesDataExist() beide true zurückgegeben haben, was bedeutet, dass sie unter Annahme, dass die Daten vorhanden sind, ausgeführt wird.

Warnung

Wenn die Daten nicht immer auf dem Ziel-DataHolder existiert, zum Beispiel, wenn die remove() Funktion erfolgreich war (siehe weiter oben), ist es unbedingt notwendig, dass du die doesDataExist()-Methode implementierst, sodass sie true zurückgibt, wenn Daten vorhanden sind, und false, wenn nicht.

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

Füll-Methoden

Eine Filler-Methode unterscheidet sich von einer Getter-Methode darin, dass sie einen DataManipulator erhaält, um ihn mit Werten zu füllen. Diese Werte kommen entweder von einem DataHolder oder müssen von einem DataContainer deserialisiert werden. Die Methode gibt ein Optional.empty() zurück, falls der DataHolder inkompatibel ist.

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

Die fill()-Methode gibt einen Optional der veränderten healthData zurück, wenn - und nur dann - alle benötigten Daten vom DataContainer erhalten werden konnten.

Andere Methoden

Abhängig von der verwendeten abstrakten Oberklasse sin möglicherweise einige andere Methoden erforderlich. Zum Beispiel muss der AbstractEntityDataProcessor DataManipulator-Instanzen an verschiedenen Stellen erstellen. Er kann das nicht, da er weder die Implementierungsklasse, noch den Konstruktor zu benutzen weiß. Daher nutzt er eine abstrakte Funktion, die von der finalen Implementierung bereitgestellt werden muss. Diese tut nicht mehr, als einen DataManipulator mit den Standarddaten zu erstellen.

Wenn du deinen DataManipulator wie empfohlen implementiert hast, kannst du einfach den no-Args-Konstruktor verwenden.

@Override
protected HealthData createManipulator() {
    return new SpongeHealthData();
}

4. Implementierung der ValueProcessoren

Nicht nur ein DataManipulator``kann einem ``DataHolder übergeben werden, sondern auch ein verschlüsselter Value``selbst. Dafür musst du mindestens einen ``ValueProcessor für jeden Key, der im DataManipulator ist, übergeben. Ein ValueProcessor wird nach dem konstanten Name seines Key``s in der ``Keys-Klasse in einer Weise, ähnlich seines DataQuery``s, benannt. Der konstante Name ist deiner Unterstriche beraubt, in CamelCase Großbuchstaben geschrieben und dann mit dem Suffix ``ValueProcessor versehen.

Ein ValueProcessor sollte immer vom AbstractSpongeValueProcessor erben, der bereits ein paar der supports()-Checks, basierend auf dem Typ des DataHolders, durchführt. Für Keys.HEALTH, werden wir einen HealthValueProcessor wie folgt erstellen und konstruieren.

public class HealthValueProcessor
        extends AbstractSpongeValueProcessor<EntityLivingBase, Double, MutableBoundedValue<Double>> {

    public HealthValueProcessor() {
        super(EntityLivingBase.class, Keys.HEALTH);
    }

    [...]

}

Jetzt wird uns der AbstractSpongeValueProcessor von der Notwendigkeit entlasten, zu überprüfen, ob der Wert unterstützt wird. Es wird angenommen, dass er unterstützt wird, wenn der Ziel-ValueContainer vom Typ EntityLivingBase ist.

Tipp

Für besser abgestimmte Kontrolle, welche EntityLivingBase-Objekte unterstützt werden können, kann die Methode supports(EntityLivingBase) überschrieben werden.

Wieder einmal wird die meiste Arbeit von der Abstraktionsklasse getan. Wir müssten nur zwei Hilfsmethoden implementieren, um einen Value und das unveränderliche Gegenstück zu erstellen, und drei weitere, um Daten zu bekommen, zu setzen und zu löschen.

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

Da es für ein EntityLivingBase unmöglich ist, keine Lebenspunkte zu haben, wird diese Methode niemals ein Optional.empty() zurückgeben.

@Override
protected boolean set(EntityLivingBase container, Double value) {
    if (value >= 0 && value <= (double) Float.MAX_VALUE) {
        container.setHealth(value.floatValue());
        return true;
    }
    return false;
}

Die set()-Methode wird Wahrheitswert zurückgeben, der angibt, ob der Wert erfolgreich gesetzt werden könnte. Diese Implementierung weist Werte außerhalb der Grenzen, unserer werterstellenden Methoden, ab.

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

Da die Daten auf jeden Fall anwesend ist, werden Versuche, sie zu löschen, immer fehlschlagen.

6. Registrierung der Prozessoren

Damit es Sponge möglich ist, unsere Manipulatoren und Prozessoren zu verwenden, müssen wir sie registrieren. Das passiert in der DataRegistrar-Klasse. In der setupSerialization()-Methode gibt es zwei große Blöcke von Registrierungen, zu welchen wir unsere Prozessoren hinzufügen werden.

DataProcessors

Ein DataProcessor ist neben Schnittstelle und den Implementierungsklassen eines DataManipulators, den er verarbeitet, registriert. Für jedes Paar von veränderlichen / nicht veränderlichen DataManipulatoren muss mindestens ein DataProcessor registriert sein.

DataUtil.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
        ImmutableHealthData.class, ImmutableSpongeHealthData.class,
        new HealthDataProcessor());

ValueProcessors

Wert-Prozessoren sind am unteren Ende derselben Funktion registriert. Für jeden Key können mehrere Prozessoren durch nachfolgende Aufrufe der registerValueProcessor()-Methode registriert sein.

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

Implementierung von Blockdaten

Blockdaten sind dadurch ein kleines bisschen anders, als andere Typen von Daten, als dass sie durch das mischen in den Block selber implementiert sind. Es gibt mehrere Methoden in org.spongepowered.mixin.core.block.MixinBlock, die nur überschrieben werden müssen, um Daten für Blöcke zu implementieren.

@Mixin(BlockHorizontal.class)
public abstract class MixinBlockHorizontal extends MixinBlock {

    [...]

}

supports() sollte true zurückgeben, wenn entweder das ImmutableDataManipulator-Interface von der Class zugewiesen werden kann, die als Argument übergeben wurde, oder die Superklasse es ermöglicht.

@Override
public boolean supports(Class<? extends ImmutableDataManipulator<?, ?>> immutable) {
    return super.supports(immutable) || ImmutableDirectionalData.class.isAssignableFrom(immutable);
}

getStateWithData()``sollte einen neuen ``BlockState``mit den Daten des ``ImmutableDataManipulator zurückgeben. Wenn der Manipulator nicht direkt unterstützt wird, sollte die Methode an die Superklasse delegieren.

@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() ist äquivalent zu getStateWithData(), aber funktioniert mit einzelnen 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);
}

Abschließend sollte getManipulators() eine Liste aller ImmutableDataManipulatoren, die der Block unterstützt, zurückgeben, zusammen mit dem aktuellen Wert des zur Verfügung gestellten IBlockState. Es sollten auch alle ImmutableDataManipulatoren dabei sein.

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

Weitere Informationen

Da Data ein eher abstraktes Konzept in Sponge ist, ist es schwer, allgemeine Anweisungen zu geben, wie an die benötigten Daten aus den Minecraft Klassen selbst gelangt werden kann. Es ist vielleicht hilfreich, die bereits implementierten Prozessoren anzuschauen, die ähnlich zu denen sind, mit denen du arbeitest, um ein besseres Verständnis zu bekommen, wie es funktionieren könnte.

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.