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:
Den DataManipulator selbst implementieren
Den
ImmutableDataManipulator
implementieren
Wenn diese Schritte abgeschlossen sind, muss folgendes noch getan werden:
Registrierung des
Key``s in der ``KeyRegistry
Implementierung des
DataProcessor
Den
ValueProcessor
für jeden Wert implementieren, den derDataManipulator
repräsentiertAlles 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 Key
s. 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önnenregistriere einen
Consumer
um den Wert direkt setzen zu könnenregistriere einen
Supplier<Value>
um einen veränderbarenValue
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änderlichenDataManipulator
sLeiten sie stattdessen von
ImmutableAbstractData
abAnstatt von
registerGettersAndSetters()
heißt die MethoderegisterGetters()
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 Key
s. 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 DataProcessor
s
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, DataHolder
s 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 ValueProcessor
en
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 DataManipulator
en 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 Key
s.
@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 ImmutableDataManipulator
en, die der Block unterstützt, zurückgeben, zusammen mit dem aktuellen Wert des zur Verfügung gestellten IBlockState
. Es sollten auch alle ImmutableDataManipulator
en 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.