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 Keys in der
KeyRegistryModule
Implementierung des
DataProcessor
Den
ValueProcessor
für jede Value implementieren, den derDataManipulator
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 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 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 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 direkt den Wert zu erhalten
registriere einen Consumer um den Wert direkt setzen zu können
registriere 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 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änderlichenDataManipulator
sLeiten sie stattdessen von
ImmutableAbstractData
abAnstatt von
registerGettersAndSetters()
heißt die MethoderegisterGetters()
When creating ImmutableDataHolder
s or ImmutableValue
s, 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 ``Key
s, 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 name
n 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 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.
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 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
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 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.
@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 DataManipulator
en 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 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.
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.