自定义数据操纵器
自定义数据的核心就是 DataManipulator 类。在实现它之前,你首先需要决定一下你的自定义数据是否需要一个单独的 API。一般说来,最好的方式是把 API 和其实现分离(SpongeAPI 就是这么做的),不过如果其他的开发者不会接触到它,那么对于这两者而言,使用同一个类也没什么问题。
你可能会为你的数据的每一个单元定义一些 API 方法,比如说 String
、 int
、 ItemStack 、或者一个自定义的类型如 Home
。这些小单元会被包装成 Value 的形式,从而可以通过数据键( Key )的方式访问。一个 Value
可能会针对不同对象,而展现出不同的扩展形式,比如说 MapValue 提供基础的键值对操作, BoundedValue 限定了一个数据的上下界(比如说整数的上下界)。对一个数据是否过界的判定基于 Comparator 类完成。
现在我们考虑你需要继承实现的 AbstractData 类型。虽然你可以从零开始,但这些类型已经为你减少了 很多 诸如实现需要的方法的工作。你可以在 org.spongepowered.api.data.manipulator.mutable.common 里找到一个完整的列表。请参阅 单一数据类型 或者 复合数据类型 以了解关于每一种类型的实现细节。
你需要创建两个不同的类——一个用于实现可变的数据操纵器,也就是实现 DataManipulator 和你的抽象类型,一个用于不可变数据操纵器,也就是实现 ImmutableDataManipulator 和你的 不可变 抽象类型。
注解
数据 API 要求 所有 的数据都必须实现可变的和不可变的类型,所以请乖乖地把它们俩都实现了。
对于所有类型,你都需要定义 DataManipulator#asImmutable() 和 asMutable() 两个方法——你只需要把当前的对象传入另一种类型的类的构造方法就行了。
数据值
你的相应 Getter 需要返回一个数据值,不过如下所示,我们为你提供了 ValueFactory 帮你代劳一部分工作。因为 Sponge 已经实现了一些 Value
对象,所以说这可以大大减少不必要的数据值类型。产生不同类型的数据值取决于使用不同的方法,如 createMapValue
、 createBoundedComparableValue
等。
代码示例:实现一个数据值的相关 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()
方法解决这一问题。我们强烈建议你对不可变数据值实施缓存(如存储入一个类的字段)。
每种类型的数据值也都需要一个数据键,也就是 Key 类用于唯一标识符,你可以看看下面的 Keys.DEFAULT_HOME
的示例。和数据值类似,你可以通过使用 KeyFactory 类的诸如 makeXKey()
的方法以创建你自己的数据键。
你需要传入一个 TypeToken
用于代表你的数据值包装的 原生 类型,以及另一个 TypeToken
代表数据值类型本身。你同时需要提供一个 DataQuery ——这通常用于序列化你的数据值。还要为你的数据类型提供一个唯一的标识 ID 及名称。把这些放到一起你就可以得到一个可用于你的 Value
的 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
,你可以使用以下两种方式:
对于不包含泛型的类型,请使用
TypeToken.of(MyType.class)
如果类型包含泛型,你可以创建一个匿名类的实例:
TypeToken<MyGenericType<String>>() {}
数据序列化
如果你希望你的数据可 serializable 到 DataHolder 或者配置文件中,你必须还要实现 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")
.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()
方法获取到它。
简单数据类型
对于简单数据类型(Simple Single Type)而言,几乎所有的工作都已由对应的抽象类实现完成了。你需要做的只有:
继承相应的抽象类
将该数据操纵器对应的 Key 、对象本身、以及默认的对象(如果对象本身是 null 的话)传入构造方法
AbstractBoundedComparableData (及对应的不可变实现)需要额外的最小值和最大值用于比较,相应的 Comparator 也需要传入。
注解
对于 List
和 Mapped
单一数据类型而言,相应的 ListData
或 MappedData
(及它们的不可变版本)都需要实现。一些专用于 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 是需要实现的。
注册数据值
然后你需要执行注册操作,这样基于 数据键 的系统才能在其上工作。如欲注册,请分别针对可变和不可变数据操纵器实现 DataManipulator#registerGettersAndSetters() 或 ImmutableDataManipulator#registerGetters() 方法。
对于任何一个数据值,你都需要调用的有:
registerKeyValue(Key, Supplier)
,并传入数据键和相应的数据值的 GetterregisterFieldGetter(Key, Supplier)
,并传入数据键和相应的 裸露在外的 对象的 GetterregisterFieldSetter(Key, Consumer)
并传入数据键和相应的 Setter,如果调用的对象是一个可变的数据操纵器的话
我们十分建议使用 Java 8 的 ::
运算符以方便地传入 Supplier
和 Consumer
。
代码示例:实现相应的 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)
两个方法和实现单一数据类型类似,只不过需要考虑保存和加载多个数据值。