Implementierung des DataManipulators

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 Key``s in der ``KeyRegistry

  2. Implementierung des DataProcessor

  3. Den ValueProcessor für jeden Wert implementieren, den der DataManipulator repräsentiert

  4. Alles in der SpongeSerializationRegistry registrieren

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.

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 ``DataManipulator``en, 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 {

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

    ...

}

Da wir wissen, dass sowohl die aktuelle, als auch die maximale Gesundheit begrenzte Werte sind, müssen wir sicherstellen, dass keine Werte außerhalb dieser Grenzen weitergegeben werden. Um dies zu erreichen nutzen wir Guavas Preconditions, von welchen wir die benötigten Methoden statisch importieren.

Bemerkung

Benutze niemals die sogenannten magischen Werte (willkürliche zahlen, Wahrheitswerte etc.) in deinem Code. Suche stattdessen die org.spongeopwered.common.data.util.DataConstants-Klasse und benutze eine passende Konstante oder lege - falls nötig - eine an.

Durch das Interface definierte Accessoren

Das Interface, das wir implementieren, spezifiziert einige Methoden, um auf Value Objekte zugreifen zu können. Für HealthData sind diese health() und maxHealth(). Jeder Aufruf an diese Funktionen sollte einen neuen Value ergeben.

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

Tipp

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

Wenn kein aktueller Wert angegeben ist, gibt der Aufruf von get() auf einen Value den Standardwert zurück.

Kopieren und Serialisierung

Die zwei Methoden copy() und 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 toContainer() wird für die Serialisierung benutzt. Verwende einen MemoryDataContainer 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 immer auch eine dazugehörige DataQuery enthält, solltest du einfach direkt den passenden Key verwenden.

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

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 den Wert direkt bekommen zu können

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

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

Die Gültigkeitskriterien für diese Setter sind die selben wie die des entsprechenden Value Objektes, daher kannst du die Gültigkeitsprüfung auch an einen Aufruf von this.health().set() delegieren und anschließend den Wert direkt zu setzen this.currentHealth = value, sofern erstes keine Exception geworfen hat.

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

Wenn du einen ImmutableDataHolder oder eine ImmutableValue erstellst, bitte prüfe, ob es Sinn macht das ImmutableDataCachingUtil zu verwenden. Zum Beispiel, wenn du etwas wie WetData hast, dass nichts außer einem Boolean enthält, ist es sinnvoller einfach zwei gecachte Instanzen von ImmutableWetData vorzuhalten, einen für jeden möglichen Wert. Für Manipulatoren und Werte mit vielen möglichen Werte (wie SignData) hat sich gezeigt, dass Caching zu teuer ist.

Tipp

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

3. Registriere den Key in der KeyRegistry

Der nächste Schritt ist es deine Key``s in der ``KeyRegistry zu registrieren. Um das zu tun suche die org.spongepowered.common.data.key.KeyRegistry Klasse und finde die statische generateKeyMap() Methode. Dort musst du dann eine Zeile hinzufügen, die deine verwendeten Schlüssel (erstellt und) registriert.

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

Die keyMap enthält die Verknüpfungen von Strings (Texten) zu den Keys. Der verwendete String sollte dem kleingeschriebenen Namen der Konstante in der Hilfsklasse Keys entsprechen. Der Key selbst wird von einer der statischen Methoden in der KeyFactory erstellt. In den meisten Fällen ist dies makeSingleKey. makeSingleKey benötigt als erstes eine Referenz auf die Klasse der zu Grunde liegenden Daten. In diesem Fall ist dies ein „Double“. Zusätzlich benötigt die Methode eine Referenz auf den verwendeten Value Typ. Das dritte Argument ist die DataQuery, welche für die Serialisierung verwendet wird. Diese wird durch die statisch importierte Methode DataQuery.of() erstellt, die einen String benötigt. Dieser String sollte ebenfalls dem Konstantennamen entsprechen, ohne Unterstriche und die Groß- und Kleinschreibung sollte in Camel Case beginnend mit einem Großbuchstaben (MeinKonstantenName) umgeschrieben werden werden.

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.

Als Namen solltest du den Namen des DataManipulator Interfaces verwenden und diesem Processor anhängen. Beispielsweise würden wir für HealthData einen HealthDataProcessor erstellen.

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 DataProcessor``en 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

Geben immer einen Wahrheitswert zurück. Wenn die Methode supports() aufgerufen wird, sollte sie eine generelle Überprüfung ausführen, ob das angegebene Ziel die Art der Daten, die von unserem DataProcessor übergeben werden, unterstützt.

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.

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.

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

Tipp

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

Warnung

Vor allem beim Arbeiten mit ItemStack``s ist es wahrscheinlich, dass du direkt mit ``NBTTagCompound``s umgehen musst. Viele NBT-Tags sind schon als konstante Variablen in der ``org.spongepowered.common.data.util.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. In solchen Fällen sollte die doesDataExist()-Methode überschrieben werden.

Da ein lebendes Entity immer Gesundheit (Hp) hat, gibt es immer HealthData und das Entfernen daher nicht unterstützt. Deshalb geben wir einfach failNoData() zurück und überschreiben die doesDataExist()-Methode nicht.

public DataTransactionResult remove(DataHolder dataHolder) {
    return DataTransactionBuilder.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 aufrufst, sodass sie true zurückgibt, wenn Daten vorhanden sind, und false, wenn nicht.

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

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 handhabt bereits das Füllen aus DataHolder durch die Erstellung eines DataManipulators vom Holder und dessen Zusammenführung mit dem übergebenen Manipulator, aber die `DataContainer-Deserialisierung kann er nicht bieten.

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

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.

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.

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

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

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

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.

public DataTransactionResult removeFrom(ValueContainer<?> container) {
    return DataTransactionBuilder.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 org.spongepowered.common.data.SpongeSerializationRegistry-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.

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

dataRegistry.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
dataRegistry.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.

Wenn du nicht weiter kommst oder dir unsicher bist, besuche den IRC Channel #spongedev, das Forum or öffne ein Issue auf GitHub. Schau dir auch die Data Processor Implementation Checklist für allgemeine Informationen an, wie die dich einbringen kannst.