自定義 DataManipulators

自定義資料的核心是 DataManipulator 。在實作之前,你必須先決定是否需要為自定義資料建立一個分離的 API。一般而言最好將 API 與實作分離(如同 SpongeAPI),但是如果不會被其他開發者使用你也可以將兩者放在同一個類別。

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

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

你需要創建兩個不同的類別, 一個是可變的版本,實作 DataManipulator 和你的抽象型態;以及不可變的版本,實作 ImmutableDataManipulator 和你的 不可變 抽象型態。

備註

全部 資料都必須有可變與不可變版本,你必須兩個都實作

對於所有型態,你都需要定義 DataManipulator#asImmutable()asMutable() 方法,只需要簡單的複製目前物件到另一個版本的建構子即可。

数据值

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

程式碼範例:實作 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);
}

請注意 ImmutableDataManipulator 必須回傳一個 ImmutableValue,可以對回傳的 Value 呼叫 asImmutable()。我們建議你對不可變版本的值做快取(例如類別的資料成員)。

每個 Value 都需要一個 Key 來識別它,像是在範例中的 Keys.DEFAULT_HOME 。與 value 相似,你可以使用 KeyFactorymakeXKey() 方法來為你的 value 創建一個 Key

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

程式碼範例:創建 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");

備註

作为一个保留泛型的类型引用的实现,Sponge 在 TypeTokens 类中内置了一大串能够用到的 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 或其他插件以一种通用的方式使用它。游戏的服务端或相应的插件可以为你的数据创建副本、并序列化或反序列化你的数据,而不需要对你代码中的类加以直接引用。

要註冊 DataManipulator Sponge 提供了 DataRegistration#builder()。它會生成 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")
      .buildAndRegister(myPluginContainer);
}

警告

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

單一型態

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

「簡易」抽象型態最容易實作,但它僅限於下列型態:

  • Boolean

  • Comparable

  • Integer

  • List

  • Map

  • CatalogType

  • Enum

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

小訣竅

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

簡易單一型態

幾乎所有工作都以經完成了。你需要做的只有:

  • 繼承對應的抽象型態

  • 對應 Key 到你的資料、物件本身、以及建構子的預設物件(如果物件為 null)

AbstractBoundedComparableData (及對應的不可變版本)還需要用於比較的最大及最小值,以及對應的 Comparator

備註

ListMapped 單一型態必須使用 ListDataMappedData(或對應的不可變版本)。這會為 DataManipulator 增加類似於操作 Map 或 List 的方法。

下列三個方法必須定義在可變的 manipluators 中:

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) 應該從 container 中取得並覆蓋 value ,如果不存在則回傳 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 是需要实现的。

註冊 Values

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

對所有 value 你必須呼叫:

  • registerKeyValue(Key, Supplier) 傳入對應 key 的 Value getter

  • registerFieldGetter(Key, Supplier) 傳入上面定義的原始物件 getter 方法

  • registerFieldSetter(Key, Consumer) 傳入 setter 方法,如果你正在實作可變版本

我們建議使用 Java 8 的 :: 語法來簡化 SupplierConsumer 函數

程式碼範例:實作 Getters 與 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)from(DataContainer) 的實作和單一型態類似,但必須處理所有的 values