Benutzerdefinierte DataManipulators

Der Kernbestandteil von benutzerdefinierten Daten ist der DataManipulator. Um diesen zu implementieren, muss du dich als erstes entscheiden, ob du eine separate API für deine Daten erstellen möchtest. Für gewöhnlich ist es am besten die API von der Implementieren zu trennen (wie es die SpongeAPI tut), aber wenn es niemals von anderen Entwicklern benutzt werden wird, dann kannst du beides auch in eine Klasse tun.

Du wirst für jedes „Teil“ in deinen Daten, wie beispielsweise einen String, int, ItemStack oder einen selbsterstellten Typ wie Home, eine eigene API Methode erstellen wollen. Diese Teile werden in einen Value gepackt, der es ermöglicht darauf mit Hilfe von Keys zuzugreifen. Es gibt verschiedene Erweiterungen von Value, je nach dem welches Object dargestellt wird, wie zum Beispiel einen MapValue, welche die Standard-Map Operationen anbietet oder einen :javadoc:`BoundedComparableValue` welcher die Ober- und Untergrenzen eines Comparable Objektes wie Integer festlegt.

Now, pick which of the AbstractData types you’ll extend from. While you could implement from scratch, these abstract types remove a lot of the work that needs to be done implementing the required methods. A full list can be found in org.spongepowered.api.data.manipulator.mutable.common. See either Single Types or Compound Types below for implementation details each type.

You need to create two different classes - one which is mutable and implements DataManipulator and your abstract type, and an immutable version which implements ImmutableDataManipulator and your immutable abstract type.

Bemerkung

All data must have mutable and immutable versions, you must implement both.

For all types, you’ll need to define the DataManipulator#asImmutable()/ asMutable() methods - this is as simple as copying the existing objects into a constructor for the alternate version.

Werte

Your value getter(s) need to return a value. In the example below, we get the ValueFactory. This saves us a lot of type by using Sponge’s already implemented Value objects. Depending on what value you’re creating there a different methods to call such as createMapValue, createBoundedComparableValue, etc.

Code Example: Implementing a Value Getter

import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.value.ValueFactory;
import org.spongepowered.api.data.value.mutable.Value;

import org.spongepowered.cookbook.myhomes.data.home.Home;
import org.spongepowered.cookbook.myhomes.data.Keys;

@Override
protected Value<Home> defaultHome() {
    return Sponge.getRegistry().getValueFactory()
            .createValue(Keys.DEFAULT_HOME, getValue(), null);
}

Note that an ImmutableDataManipulator would instead return an ImmutableValue, by calling asImmutable() on the returned Value. We recommended that you cache this (such as with a class field) in the immutable version.

Each Value also needs a Key to identify it, seen in the example as Keys.DEFAULT_HOME. Similar to values, you use one of the makeXKey() methods in KeyFactory to create a Key for your value.

You need to pass one TypeToken representing the raw type of your value, and one TypeToken representing the Value. You also need to provide a DataQuery path - this is most commonly used to serialize the Value. As with any catalog type you must also provide a unique ID and a name. Put this all together and you have a Key you can use in your Values.

Code Example: Creating a Key

import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.key.Key;
import org.spongepowered.api.data.key.KeyFactory;
import org.spongepowered.api.data.value.mutable.Value;
import org.spongepowered.api.data.value.mutable.Value;

import com.google.common.reflect.TypeToken;

import org.spongepowered.cookbook.myhomes.data.home.Home;

public static final Key<Value<Home>> DEFAULT_HOME = KeyFactory.makeSingleKey(
        TypeToken.of(Home.class),
        new TypeToken<Value<Home>>() {},
        DataQuery.of("DefaultHome"), "myhomes:default_home", "Default Home");

Bemerkung

TypeTokens are used by the implementation to preserve the generic type of your values. Sponge provides a long list of pre-built tokens for the API in TypeTokens.

If you need to create your own, you can do this in one of two ways:

  • For non-generic types, use TypeToken.of(MyType.class)

  • For generic types, create an anonymous class with TypeToken<MyGenericType<String>>() {}

Serialization

To make your data serializable to DataHolders or config files, you must also implement DataSerializable#toContainer(). We recommend calling super.toContainer() as this will include the version from DataSerializable#getContentVersion(). You should increase the version each time a change is made to the format of your serialized data, and use DataContentUpdaters to allow backwards compatability.

Bemerkung

This is not required for simple single types, as the already implement toContainer()

Code Example: Implementing toContainer

import org.spongepowered.api.data.DataContainer;

import org.spongepowered.cookbook.myhomes.data.Keys;

@Override
public DataContainer toContainer() {
    DataContainer container = super.toContainer();
    // This is the simplest, but use whatever structure you want!
    container.set(Keys.DEFAULT_HOME.getQuery(), this.defaultHome);
    container.set(Keys.HOMES, this.homes);

    return container;
}

Registration

Registering your DataManipulator allows it to be accessible by Sponge and by other plugins in a generic way. The game/plugin can create copies of your data and serialize/deserialize your data without referencing any of your classes directly.

To register a DataManipulator Sponge has the DataRegistration#builder() helper. This will build a DataRegistration and automatically register it.

Bemerkung

Due to the nature of Data, you must register your DataManipulator during initialization - generally by listening to GameInitializationEvent such as in the example below. If you try to register a DataManipulator once initialization is complete an exception will be thrown.

import org.spongepowered.api.event.game.state.GameInitializationEvent;
import org.spongepowered.api.data.DataRegistration;

import org.example.MyCustomData;
import org.example.ImmutableCustomData;
import org.example.CustomDataBuilder;

@Listener
public void onInit(GameInitializationEvent event) {
  DataRegistration.builder()
      .dataClass(MyCustomData.class)
      .immutableClass(ImmutableCustomData.class)
      .builder(new CustomDataBuilder())
      .manipulatorId("my-custom")
      .dataName("My Custom")
      .buildAndRegister(myPluginContainer);
}

Warnung

Daten, die vor 6.0.0 serialisiert worden sind, oder Daten, bei dennen du die ID geändert hast, werden nicht bemmerkt, außer mit DataManager#registerLegacyManipulatorIds(String, DataRegistration) registriert. Beim Registrieren eines prä-6.0.0 DataManipulator wird die ID von Class.getName() genommen, wie com.example.MyCustomData.

Single Types

Single types require little implementation because much of the work has already been done in the AbstractSingleData type you extend from.

The „simple“ abstract types are the easiest to implement, but are restricted to only the types below:

  • Boolean

  • Comparable

  • Integer

  • List

  • Map

  • CatalogType

  • Enum

For all other types you must implement a custom single type by extending AbstractSingleData. This allows you to define your own single data with whatever type you want, while still doing most of the work for you.

Tipp

The abstract implementations save the object for you in the constructor. You can access it in your implementation by calling the getValue() and getValueGetter() methods.

Simple Single Types

Almost all the work is done for you with simple abstract types. All you need to do is:

  • Extend the relevant abstract type

  • pass the Key for your data, the object itself, and the default object (if the object is null) in the constructor

AbstractBoundedComparableData (and the immutable equivalent) additionally require minimum and maximum values that will be checked, as well as a Comparator.

Bemerkung

List and Mapped single types must instead implement ListData / MappedData (or the immutable equivalent). This adds additional methods to allow Map-like/List-like behavior directly on the DataManipulator.

The following 3 methods must be defined on mutable manipluators:

fill(DataHolder, MergeFunction) should replace the data on your object with that of the given DataHolder, using the result of MergeFunction#merge().

import org.spongepowered.api.data.DataHolder;
import org.spongepowered.api.data.merge.MergeFunction;

import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;

import java.util.Optional;

@Override
public Optional<FriendsData> fill(DataHolder dataHolder, MergeFunction overlap) {
    FriendsData merged = overlap.merge(this, dataHolder.get(FriendsData.class).orElse(null));
    setValue(merged.friends().get());

    return Optional.of(this);
}

from(DataContainer) should overwrite its value with the one in the container and return itself, otherwise return Optional.empty()

import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.DataQuery;

import org.spongepowered.cookbook.myhomes.data.Keys;
import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;
import org.spongepowered.cookbook.myhomes.data.friends.ImmutableFriendsData;

import com.google.common.collect.Maps;

import java.util.Optional;
import java.util.UUID;

@Override
public Optional<FriendsData> from(DataContainer container) {
    if(container.contains(Keys.FRIENDS)) {
        List<UUID> friends = container.getObjectList(Keys.FRIENDS.getQuery(), UUID.class).get();
        return Optional.of(setValue(friends));
    }

    return Optional.empty();
}

copy() should, as the name suggests, return a copy of itself with the same data.

import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;

@Override
public FriendsData copy() {
    return new FriendsDataImpl(getValue());
}

Custom Single Types

In addition to the , you need to override the following methods:

getValueGetter() should pass the Value representing your data (see above).

toContainer() should return a DataContainer representing your data (see above).

Compound Types

Whereas single types only support one value, „compound“ types support however many values you want. This is useful when multiple objects are grouped, such as FurnaceData. The downside, however, is that they are more complex to implement.

To start with, create all the Value getters that your data will have. For each value, create a method to get and set the raw object, which you’ll use later. For immutable data, only the getters are necessary.

Registering Values

Next, you’ll want to register these so that the Keys-based system can reference them. To do this, implement either DataManipulator#registerGettersAndSetters() or ImmutableDataManipulator#registerGetters() depending on whether the data is mutable or not.

For each value you must call:

  • registerKeyValue(Key, Supplier) referencing the Value getter for the given key

  • registerFieldGetter(Key, Supplier) referencing the getter method for the raw object defined above

  • registerFieldSetter(Key, Consumer) referencing the setter method above if you are implementing the mutable version

We recommend using Java 8’s :: syntax for easy Supplier and Consumer functions.

Code Example: Implementing Getters and Setters

import org.spongepowered.cookbook.myhomes.data.Keys

// registerGetters() for immutable implementation
@Override
protected void registerGettersAndSetters() {
    registerKeyValue(Keys.DEFAULT_HOME, this::defaultHome);
    registerKeyValue(Keys.HOMES, this::homes);

    registerFieldGetter(Keys.DEFAULT_HOME, this::getDefaultHome);
    registerFieldGetter(Keys.HOMES, this::getHomes);

    // Only on mutable implementation
    registerFieldSetter(Keys.DEFAULT_HOME, this::setDefaultHome);
    registerFieldSetter(Keys.HOMES, this::setHomes);
}

fill(DataHolder, MergeFunction) and from(DataContainer) are similar to the implementations for single data, but loading all your values.