自定义数据操纵器

自定义数据的核心就是 DataManipulator 类。在实现它之前,你首先需要决定一下你的自定义数据是否需要一个单独的 API。一般说来,最好的方式是把 API 和其实现分离(SpongeAPI 就是这么做的),不过如果其他的开发者不会接触到它,那么对于这两者而言,使用同一个类也没什么问题。

你可能会为你的数据的每一个单元定义一些 API 方法,比如说 StringintItemStack 、或者一个自定义的类型如 Home 。这些小单元会被包装成 Value 的形式,从而可以通过数据键( Key )的方式访问。一个 Value 可能会针对不同对象,而展现出不同的扩展形式,比如说 MapValue 提供基础的键值对操作, MutableBoundedValue 限定了一个数据的上下界(比如说整数的上下界)。对一个数据是否过界的判定基于 Comparator 类完成。

现在我们考虑你需要继承实现的 AbstractData 类型。虽然你可以从零开始,但这些类型已经为你减少了 很多 诸如实现需要的方法的工作。你可以在 org.spongepowered.api.data.manipulator.mutable.common 里找到一个完整的列表。请参阅 单一数据类型 或者 复合数据类型 以了解关于每一种类型的实现细节。

你需要创建两个不同的类——一个用于实现可变的数据操纵器,也就是实现 DataManipulator 和你的抽象类型,一个用于不可变数据操纵器,也就是实现 ImmutableDataManipulator 和你的 不可变 抽象类型。

注解

数据 API 要求 所有 的数据都必须实现可变的和不可变的类型,所以请乖乖地把它们俩都实现了。

对于所有类型,你都需要定义 DataManipulator#asImmutable()asMutable() 两个方法——你只需要把当前的对象传入另一种类型的类的构造方法就行了。

数据值

你的相应 Getter 需要返回一个数据值,不过如下所示,我们为你提供了 ValueFactory 帮你代劳一部分工作。因为 Sponge 已经实现了一些 Value 对象,所以说这可以大大减少不必要的数据值类型。产生不同类型的数据值取决于使用不同的方法,如 createMapValuecreateBoundedComparableValue 等。

代码示例:实现一个数据值的相关 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);
}

请注意 ImmutableDataManipulator 需要返回 ImmutableValue 类的实例,不过你可以调用返回的 ValueasImmutable() 方法解决这一问题。我们强烈建议你对不可变数据值实施缓存(如存储入一个类的字段)。

每种类型的数据值也都需要一个数据键,也就是 Key 类用于唯一标识符,你可以看看下面的 Keys.DEFAULT_HOME 的示例。和数据值类似,你可以通过使用 KeyFactory 类的诸如 makeXKey() 的方法以创建你自己的数据键。

你需要传入一个 TypeToken 用于代表你的数据值包装的 原生 类型,以及另一个 TypeToken 代表数据值类型本身。你同时需要提供一个 DataQuery ——这通常用于序列化你的数据值。还要为你的数据类型提供一个唯一的标识 ID 及名称。把这些放到一起你就可以得到一个可用于你的 ValueKey 了。

代码示例:创建数据键

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

注解

作为一个保留泛型的类型引用的实现,Sponge 在 TypeTokens 类中内置了一大串能够用到的 TypeToken

如果你想要自己创建一个 TypeToken ,你可以使用以下两种方式:

  • 对于不包含泛型的类型,请使用 TypeToken.of(MyType.class)

  • 如果类型包含泛型,你可以创建一个匿名类的实例: TypeToken<MyGenericType<String>>() {}

数据序列化

如果你希望你的数据可 serializableDataHolder 或者配置文件中,你必须还要实现 DataSerializable#toContainer() 方法。我们十分建议你调用父类的 super.toContainer() ,这样子就会包含 DataSerializable#getContentVersion() 提供的版本号信息。每次数据信息的结构发生变化时你应该提升你的版本号信息,并使用 DataContentUpdater 类 以保证向前兼容。

注解

对于单一数据类型你不需要这么做,因为它已经实现 toContainer() 方法了

代码示例:实现 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;
}

注册

通过注册你自定义的 DataManipulator ,你可以让 Sponge 或其他插件以一种通用的方式使用它。游戏的服务端或相应的插件可以为你的数据创建副本、并序列化或反序列化你的数据,而不需要对你代码中的类加以直接引用。

Sponge 提供了 DataRegistration#builder() 方法以帮助你注册一个 DataManipulator 。这将生成一个 DataRegistration 并自动加以注册。

注解

由于 Sponge 的数据 API 的性质,你 必须 要在初始化阶段中的特定阶段注册你的 DataManipulator ——通过监听 GameInitializationEvent ,如下例所示。如果你想要在该阶段结束后注册,那么游戏将抛出一个异常。

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

警告

6.0.0 版本之前序列化的数据,或者你已经更换了 ID 的数据,将不会被自动识别出来,除非你使用了 DataManager#registerLegacyManipulatorIds(String, DataRegistration) 方法注册。如果你以 6.0.0 版本之前提供的方式注册了 DataManipulator ,那么相应的ID将会是 Class.getName() 的返回值,如 com.example.MyCustomData

单一数据类型

实现单一数据类型(Single Type)需要的工作要少些,因为很多方法都已经被你需要继承实现的 :javadoc:AbstractSingleData 类实现了。

简单数据类型实现起来难度最低,不过只限于以下类型:

  • Boolean

  • Comparable

  • Integer

  • List

  • Map

  • CatalogType

  • Enum

对于所有其他类型,你都需要通过继承实现 AbstractSingleData 类来实现它们。这允许你定义你自己的单一数据类型,而不受数据类型的约束,不过你就需要做较多的工作了。

小技巧

默认的实现类已经在构造方法中把你的对象准备好了。你可以通过 getValue() 或者 getValueGetter() 方法获取到它。

简单数据类型

对于简单数据类型(Simple Single Type)而言,几乎所有的工作都已由对应的抽象类实现完成了。你需要做的只有:

  • 继承相应的抽象类

  • 将该数据操纵器对应的 Key 、对象本身、以及默认的对象(如果对象本身是 null 的话)传入构造方法

AbstractBoundedComparableData (及对应的不可变实现)需要额外的最小值和最大值用于比较,相应的 Comparator 也需要传入。

注解

对于 ListMapped 单一数据类型而言,相应的 ListDataMappedData (及它们的不可变版本)都需要实现。一些专用于 List 和 Map 的数据操作方法都会被额外加入到 DataManipulator 中。

下面三个方法必须由可变数据操纵器实现:

fill(DataHolder, MergeFunction) 方法用于把自己的数据操纵器中的数据,通过 MergeFunction#merge() 方法替换成给定 DataHolder 中的数据。

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) 用于基于给定的 DataContainer 覆盖已有数据,如果相应数据不存在则返回 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() ,顾名思义,返回一个当前数据操纵器的副本。

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

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

自定义单一数据类型

在简单单一类型的方法的基础上,你还需要覆写下列方法:

getValueGetter() 方法的返回值需要返回一个可以代表你的数据的 Value (见上)。

toContainer() 方法需要返回一个代表你的数据的 DataContainer (见上)。

复合数据类型

和只能提供一种数据值的单一数据类型相比,复合数据类型可以提供多个“复合”的数据值。如果多个数据值是强关联的,像 FurnaceData 一样,那么复合数据类型便可大显身手,不过,实现复合数据类型,也就会变得更复杂一些。

首先请定义你的数据操纵器的所有数据值的 Getter。对于每一个数据值,请再创建相应的 裸露在外的 对象的 Getter 和 Setter。你马上就会用到它们。对于不可变数据操纵器来说,只有 Getter 是需要实现的。

注册数据值

然后你需要执行注册操作,这样基于 数据键 的系统才能在其上工作。如欲注册,请分别针对可变和不可变数据操纵器实现 AbstractData#registerGettersAndSetters()AbstractImmutableData#registerGetters() 方法。

对于任何一个数据值,你都需要调用的有:

  • registerKeyValue(Key, Supplier) ,并传入数据键和相应的数据值的 Getter

  • registerFieldGetter(Key, Supplier) ,并传入数据键和相应的 裸露在外的 对象的 Getter

  • registerFieldSetter(Key, Consumer) 并传入数据键和相应的 Setter,如果调用的对象是一个可变的数据操纵器的话

我们十分建议使用 Java 8 的 :: 运算符以方便地传入 SupplierConsumer

代码示例:实现相应的 Getter 和 Setter

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)from(DataContainer) 两个方法和实现单一数据类型类似,只不过需要考虑保存和加载多个数据值。