实现数据操纵器¶
这是一个创建数据操纵器(DataManipulator)的指南,贡献者可以通过本指南来学习如何帮助实现数据 API(Data API)。一个实现数据操纵器的更新目录可以在 SpongeCommon Issue #8 被找到。
若要完整实现一个 DataManipulator 必须遵循这些步骤:
- 实现
DataManipulator
本身 - 实现 ImmutableDataManipulator
当这些步骤完成后,下列的这些事情也要完成:
- 在
KeyRegistryModule
中注册数据键(Key) - 实现数据处理器(
DataProcessor
) - 为每个表示
DataManipulator
的值(Value)实现数据值处理器(ValueProcessor
)
若数据是附加到方块上的,你还需要向方块中混入 (mix in to) 若干方法。
注解
确保你遵守了 贡献指南 。
下面这个代码片段展示了 SpongeCommon 中的几个类的 import,你在实际操作中也会需要导入这些类:
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.data.util.DataConstants;
import org.spongepowered.common.data.util.NbtDataUtil;
import org.spongepowered.common.registry.type.data.KeyRegistryModule;
1. 实现数据操纵器(DataManipulator)¶
DataManipulator
的实现的约定是把它们加上 Sponge
的前缀。也就是说,为了实现 HealthData 接口,我们在合适的包创建了一个名为 SpongeHealthData
的类。为了实现 DataManipulator
的一部分内容,我们先在 org.spongepowered.common.data.manipulator.mutable.common
中新建了一个合适的抽象类。最常用的一般是 AbstractData
,不过我们还有一些抽象类可以大大减少不必要的代码,甚至我们为一些特殊情况,如只包含有一个数据的 DataManipulator
提供了抽象。
public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {
[...]
}
AbstractData
类有两个类型参数。第一个是实现它的类本身,第二个是它对应的 ImmutableDataManipulator
接口的实现。
构造方法¶
在大多数情况下,实现一个数据操纵器,往往需要编写两个构造方法:
- 第一个构造方法没有参数,而它同时调用另一个构造方法。并传入“默认”的参数
- 第二个构造方法需要传入所有需求并支持的参数。
第二个构造方法必须
- 调用
AbstractData
的构造方法,并把对应的 Class 类实例传递进去。 - 确保传递的值合法
- 调用
registerGettersAndSetters()
方法
import static com.google.common.base.Preconditions.checkArgument;
import org.spongepowered.common.data.util.DataConstants;
public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {
private double health;
private double maxHealth;
public SpongeHealthData() {
this(DataConstants.DEFAULT_HEALTH, DataConstants.DEFAULT_HEALTH);
}
public SpongeHealthData(double health, double maxHealth) {
super(HealthData.class);
checkArgument(maxHealth > DataConstants.MINIMUM_HEALTH);
this.health = health;
this.maxHealth = maxHealth;
registerGettersAndSetters();
}
[...]
}
因为我们知道,不管是生命值,还是最大生命值,都有着上下限,所以我们要确保传入的值不会超出上下限。我们可以使用 Guava 的 Preconditions
类,并把其中的若干方法以静态方式导入(译者注:import static)。
注解
永远不要在你的代码中使用魔数(Magic Number,如任意数字、布尔值等)。请使用 DataConstants
中提供的常数——或者在需要的时候自己创建一个。
接口定义的访问器¶
我们实现的接口定义了若干个方法用于访问数据值(Value)对象。对于 HealthData
来说,我们有 HealthData#health() 和 HealthData#maxHealth() 两个方法。每一次对相关方法的调用都应该生成一份新的 Value
。
public MutableBoundedValue<Double> health() {
return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
.minimum(DataConstants.MINIMUM_HEALTH)
.maximum(this.maximumHealth)
.defaultValue(this.maximumHealth)
.actualValue(this.currentHealth)
.build();
}
小技巧
因为 Double
实现了 Comparable
,所有我们并不需要显式指定一个 Comparable
类的实现。
如果当前值没有指定,那么调用 Value
类的 BaseValue#get() 将返回默认值。
复制和序列化¶
实现 DataManipulator#copy() 和 DataManipulator#asImmutable() 两个方法并不需要做太多的工作,你只需要根据现有的数据,分别创建对应的可变的和不可变的数据操纵器副本就可以了。
DataSerializable#toContainer() 方法用于序列化。请使用 DataContainer#createNew() 作为返回值并把相关的数据放进去。一个 DataContainer 本身可以看做 DataQuery 到对应值的映射。因为每个 Key 都有对应的 DataQuery
,所有只需要直接传入相应的 Key
就可以获得对应的值了。
public DataContainer toContainer() {
return DataContainer.createNew()
.set(Keys.HEALTH, this.currentHealth)
.set(Keys.MAX_HEALTH, this.maximumHealth);
}
registerGettersAndSetters()
方法¶
一个 DataManipulator
同时提供了方法用于根据数据键获取和设置数据。当然,我们的 AbstractData
提供了相关实现,不过我们必须告诉它我们怎么获取数据,以及获取的是什么数据。因此,我们需要在 registerGettersAndSetters()
方法的实现中为我们的每一个数据值做下面几件事:
Supplier
和 Consumer
都可以使用 Java8 的 Lambda 表达式。
private void setCurrentHealthIfValid(double value) {
if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
this.currentHealth = value;
} else {
throw new IllegalArgumentException("Invalid value for current health");
}
}
private void setMaximumHealthIfValid(double value) {
if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
this.maximumHealth = value;
} else {
throw new IllegalArgumentException("Invalid value for maximum health");
}
}
private void registerGettersAndSetters() {
registerFieldGetter(Keys.HEALTH, () -> this.currentHealth);
registerFieldSetter(Keys.HEALTH, this::setCurrentHealthIfValid);
registerKeyValue(Keys.HEALTH, this::health);
registerFieldGetter(Keys.MAX_HEALTH, () -> this.maximumHealth);
registerFieldSetter(Keys.MAX_HEALTH, this::setMaximumHealthIfValid);
registerKeyValue(Keys.MAX_HEALTH, this::maxHealth);
}
作为设置值的手段, Consumer
必须在试图设置值时进行一定的检查。尤其是当有些 DataHolder 不接受负数值的时候。所以如果传入的值不合法,该 Consumer
应该直接抛出一个 IllegalArgumentException
。
小技巧
对应的设置值时的要求标准应与其分别的 Value
对象相一致。所以你可以试着把 this.health().set()
方法传入的值检查部分隔离成另一个方法,然后直接执行 this.currentHealth = value
,如果之前的检查没有抛出异常的话。
一切就绪。你设计的 DataManipulator
的所有工作已经全部完成了。
2. 实现不可变数据操纵器(ImmutableDataManipulator)¶
实现 ImmutableDataManipulator 的过程和实现可变的数据操纵器类似。
区别:
- 对应的可变的
DataManipulator
的类名前缀变成了ImmutableSponge
- 继承的是
ImmutableAbstractData
- 不存在
registerGettersAndSetters()
方法,你只需要关心的是registerGetters()
方法
当创建不可变数据访问器( ImmutableDataHolder
)或者不可变数据值( ImmutableValue
)时,你可以通过 ImmutableDataCachingUtil
来检查它是否有意义。比如一个 WetData
只会存储一个布尔值,所以你完全可以只存储两个 ImmutableWetData
作为缓存——每个 ImmutableWetData
对应着布尔值的一种情况。不过对于可能有很多值的情况(如 SignData
),缓存数据值和数据操纵器的成本似乎就太高了些。
小技巧
你应该把 ImmutableDataManipulator
中的字段都声明为 final
以防止你可能产生的代码问题。
3. 在 KeyRegistryModule 中注册数据键(Key)¶
接下来你应该使用 Keys 注册 Key。因此,请找到 KeyRegistryModule
类的 registerDefaults()
方法。然后,在该方法里添加一行用于(创建和)注册的新代码。
this.register(Key.builder()
.type(TypeTokens.BOUNDED_DOUBLE_VALUE_TOKEN)
.id("health")
.name("Health")
.query(of("Health"))
.build());
this.register(Key.builder()
.type(TypeTokens.BOUNDED_DOUBLE_VALUE_TOKEN)
.id("max_health")
.name("Max Health")
.query(of("MaxHealth"))
.build());
调用 register(Key)
方法,注册所有的 Key
以便后续之用。注册时使用的 ID 应为 Keys
类中相应字段的小写形式。你应使用 Key#builder() 的返回值,也就是 Key.Builder 构造一个对应的 Key
的实例。你需要分别设置相应的 TypeToken
、id
、有一定可读性的 name
,和一个 DataQuery
。DataQuery
用于序列化。你应使用 DataQuery.of()
方法并传入一个字符串获取相应的实例。传入的字符串值和字段名称类似,不过应去除下划线并使用大写驼峰式。
4. 实现数据处理器(DataProcessor)¶
现在我们着手解决数据处理器( DataProcessor
)。一个 DataProcessor
负责联结 DataManipulator
和原版 Minecraft 的对象。当 DataHolder
需要处理原版 Minecraft 中的数据时, DataProcessor
或 ValueProcessor
将负责解决这些最底层的事情。
对于你的类名而言,你应该使用你在 DataManipulator
的实现中使用的名字,并附加上 Processor
的后缀。因此,对于 HealthData
而言,我们会使用 HealthDataProcessor
作为类名。
为了减少不必要的代码,你的 DataProcessor
应该实现 org.spongepowered.common.data.processor.common
中的合适的抽象类。因为只有部分实体拥有生命值和最大生命值,因此我们可以使用基于 net.minecraft.entity.Entity
的实体专用的 AbstractEntityDataProcessor
。一般情况下,通过 AbstractEntityDataProcessor
我们还可以减少一些不必要的工作,不过对于基于有着多个数据的 HealthData
的数据处理器而言不行。
public class HealthDataProcessor
extends AbstractEntityDataProcessor<EntityLivingBase, HealthData, ImmutableHealthData> {
public HealthDataProcessor() {
super(EntityLivingBase.class);
}
[...]
}
根据你使用的抽象类,你需要实现的方法也大不相同,具体取决于你使用的抽象类的实现程度。不过通常情况下,方法是有着分类的。
小技巧
你可以为同一种数据创建多个 DataProcessor
。如果针对的 DataHolder
并不相同(如 TileEntity
和 ItemStack
),你往往应该为不同的 DataHolder
分别使用不同的数据处理器的抽象类,以充分复用已有的代码。当然你要注意一下针对物品、Tile Entity、和实体的不同的 Java 包结构。
验证方法¶
一定要返回一个 boolean 值。如果有 supports(target)
调用,它应当进行一次普通检查,以确认给定的 target 是否支持你的 DataProcessor
所处理的数据。根据你抽象化程度的不同,如果你已经需要实现某个最明确的版本,那么你就没有必要实现这个方法,因为更宽泛的版本会将实现代理过去。
对于我们的 HealthDataProcessor
来说, supports()
方法已经被 AbstractEntityDataProcessor
实现了。默认情况下,它会根据 super()
构造方法传入的 Class 对象判断传入的参数是否是该 Class 对象对应的类的子类并在是时返回 true。
相反,我们需要提供一个名为 doesDataExist()
的方法。因为我们使用的抽象类并不知道如何获取到数据,所以我们需要实现它定义的这么一个方法。正如该方法名称所述,这个方法检查数据是否在支持的数据访问器上存在。对于 HealthDataProcessor
而言,这个方法总是返回 true,因为每一种实体生物都有生命值和最大生命值。
@Override
protected boolean doesDataExist(EntityLivingBase entity) {
return true;
}
设置方法¶
一个设置方法(Setter)接收一个 DataHolder
和若干需要使用的数据(如果有的话)作为参数。
DataProcessor
接口定义了一个名为 set()
的方法。该方法需要一个 DataHolder
和一个 DataManipulator
并返回一个 DataTransactionResult
。根据不同的抽象类实现,你可能不需要实现这一方法,因为抽象类已经帮你实现好了。
在这种情况下, AbstractEntityDataProcessor
解决了大部分的问题,并只需要一个方法用于设置数据,同时在设置成功时返回 true
否则返回 false
。所有关于 DataHolder
是否支持该 Data
的问题该抽象类都已解决,同时该抽象类只是创建了一个把 DataManipulator
的每个 Key
和其值相对应的映射,并由此创建一个 DataTransactionResult
,该 DataTransactionResult
决定操作是否成功。
@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;
}
小技巧
要了解 DataTransactionResult,请点击 对应的文档页面,以及参考 DataTransactionResult.Builder 的相关文档以了解如何创建。
警告
当你进行 ItemStack 的相关操作时,你可能尤其需要处理有关于 NBTTagCompound
的数据。很多的 NBT 标签名称已经在 NbtDataUtil
类中给出定义了,如果你想要的名称不存在,你应该在其中添加上你想要的名称,以防止代码中出现硬编码的魔数。
移除方法¶
remove()
方法用于把 DataHolder
中的对应数据移除并返回一个 DataTransactionResult
。
我们的任何 DataProcessor
的抽象类实现都没有去实现移除方法,因为我们也不知道相应的 DataHolder
(如 WetData
或 HealthData
)应该怎么移除数据,以及这个数据到底存在不存在(如 LoreData
)。如果数据本应总是存在,那么相应的 remove()
方法应该总是失败,如果它可能存在可能不存在,那么相应的 remove()
方法就应该移除它。
因为实体生物一定有生命值,所以 HealthData
总是存在的,也不应该支持移除。因此我们只需要返回 DataTransactionResult#failNoData() 方法的返回值。
@Override
public DataTransactionResult remove(DataHolder dataHolder) {
return DataTransactionResult.failNoData();
}
获取方法¶
一个获取方法(Getter)从一个 DataHolder
获取数据并返回一个可选的(Optional) DataManipulator
。 DataProcessor
接口定义了 from()
和 createFrom()
方法,它们两者的区别在于前者会在数据访问器支持该类型的数据,但数据不存在时返回 Optional.empty()
,而后者会在不存在的情况下提供 DataManipulator
的默认值。
再一次地,我们的 AbstractEntityDataProcessor
会提供相关的大部分实现,并仅仅需要实现获取 DataHolder
的实际值的方法。这个方法会仅在 supports()
和 doesDataExist()
两个方法都返回 true 时调用,也就是说调用时已经假定数据存在了。
警告
如果相应的 DataHolder
不存在数据,如调用 remove()
成功(见上),那么你有必要实现 doesDataExist()
方法,并在数据存在时返回 true
否则返回 false
。
@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);
}
填充方法¶
一个填充方法(Filler)和获取方法不同,因为它接收一个 DataManipulator
用于填充数据。这种数据可能由 DataHolder
提供,也可能由 DataContainer
反序列化得到。这一方法将在该 DataHolder
不支持该种数据时返回 Optional.empty()
。
我们的 AbstractEntityDataProcessor
同样通过从数据访问器创建 DataManipulator
并与已有数据操纵器合并的方式实现了相应的填充方法,不过我们可没有办法帮你包办 DataContainer
的反序列化操作。
@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);
}
fill()
方法将返回一个包含有额外的 healthData
的 Optional
,当且仅当相应的 DataContainer
存在相应的数据。
其它方法¶
根据实现的不同抽象父类,你可能需要实现一些其他的方法。例如, AbstractEntityDataProcessor
需要在不同的地方创建 DataManipulator
,如果它既不知道子类的 Class 实例,也不知道使用的构造方法,它就没有办法做到这一点。因此它利用了一个应该被实现为 final 的抽象方法。实现这个方法只需要创建一个含有默认值的 DataManipulator
就可以了。
如果你按照我们推荐的方式实现了你的 DataManipulator
,你只需要使用没有参数的构造方法就可以了。
@Override
protected HealthData createManipulator() {
return new SpongeHealthData();
}
5. 实现数据值处理器(ValueProcessor)¶
不只是 DataManipulator
需要处理有关 DataHolder
的事情, Value
也要做类似的事。因此,你需要为每个你的 DataManipulator
的 Key
提供相应的至少一个 ValueProcessor
。一个 ValueProcessor
被命名为其对应的 Keys
类里的 Key
的常量值,和相应的 DataQuery
类似。常量值应把小划线形式变成大写驼峰式,同时后面添加上名为 ValueProcessor
的后缀。
一个 ValueProcessor
应该总是继承 AbstractSpongeValueProcessor
,因为它已经处理了基于 DataHolder
类型的 supports()
方法检查。对于 Keys.HEALTH
来说,我们会创建对应的 HealthValueProcessor
,如下所示。
public class HealthValueProcessor
extends AbstractSpongeValueProcessor<EntityLivingBase, Double, MutableBoundedValue<Double>> {
public HealthValueProcessor() {
super(EntityLivingBase.class, Keys.HEALTH);
}
[...]
}
我们的 AbstractSpongeValueProcessor
可以减轻对于值是否支持的检查,这里我们已经假设对应的 ValueContainer
同时也是 EntityLivingBase
了。
小技巧
对于什么样的 EntityLivingBase
可以支持的更细粒度的控制,我们可以覆写 supports(EntityLivingBase)
方法。
同样,我们已经完成了实现的大部分工作。我们只需要实现两个方法用于创建 Value
和其对应的不可变对象,以及对应的三个获取、设置、及移除方法。
@Override
protected MutableBoundedValue<Double> constructValue(Double health) {
return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
.minimum(DataConstants.MINIMUM_HEALTH)
.maximum(((Float) Float.MAX_VALUE).doubleValue())
.defaultValue(DataConstants.DEFAULT_HEALTH)
.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());
}
因为不存在不拥有生命值和最大生命值的 EntityLivingBase
,所以这个方法也永远不会返回一个 Optional.empty()
。
@Override
protected boolean set(EntityLivingBase container, Double value) {
if (value >= DataConstants.MINIMUM_HEALTH && value <= (double) Float.MAX_VALUE) {
container.setHealth(value.floatValue());
return true;
}
return false;
}
set()
方法将返回一个布尔值,用于指示相应的值是否已被成功设置。相应的实现会对越界的值进行检查,我们之前在设计数据值的构造方法时也这么做过。
@Override
public DataTransactionResult removeFrom(ValueContainer<?> container) {
return DataTransactionResult.failNoData();
}
因为数据总是存在,所以对其的移除操作总是失败的。
6. 注册处理器¶
为使 Sponge 可以使用我们的数据操纵器、数据处理器等,我们需要注册它们。 DataRegistrar
类负责的就是这个。其中的 setupSerialization()
方法有两大块用于注册,我们也应把注册的工作添加于此处。
数据处理器¶
一个 DataProcessor
和它对应的接口及和 DataManipulator
对应的实现类一起注册。对于任何一对可变和不可变的 DataManipulator
,相应的 DataProcessor
必须至少注册一个。
DataUtil.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
ImmutableHealthData.class, ImmutableSpongeHealthData.class,
new HealthDataProcessor());
数据值处理器¶
数据值处理器以完全相同的方法底部注册。对于每一个 Key
都会有一串对于 registerValueProcessor()
方法的调用以注册这些数据值处理吕。
DataUtil.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
DataUtil.registerValueProcessor(Keys.MAX_HEALTH, new MaxHealthValueProcessor());
实现方块数据¶
方块数据和其他类型的数据不同:它是通过混入方块底层实现的。实现方块数据时,需要覆写 org.spongepowered.mixin.core.block.MixinBlock
下的若干方法。
@Mixin(BlockHorizontal.class)
public abstract class MixinBlockHorizontal extends MixinBlock {
[...]
}
当传入的 Class
对象,或传入的 Class
的超类,可以直接转换为 ImmutableDataManipulator
时, supports()
应返回 true
。
@Override
public boolean supports(Class<? extends ImmutableDataManipulator<?, ?>> immutable) {
return super.supports(immutable) || ImmutableDirectionalData.class.isAssignableFrom(immutable);
}
getStateWithData()
should return a new BlockState
with the data from the ImmutableDataManipulator
applied
to it. If the manipulator is not directly supported, the method should delegate to the superclass.
@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()
等价于 getStateWithData()
,但可以用在单一 Key
上。
@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);
}
最后, getManipulators()
应返回连同当前 IBlockState
所代表的具体数据在内的,所有该方块所支持的 ImmutalbeDataManipulator
的列表。超类所支持的 ImmutableDataManipulator
也应包含在此列表中。
@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();
}
更多信息¶
Sponge 中的 Data
是一个相当抽象的概念。我们也很难给出从原版 Minecraft 类获取需要的数据的一般指示。在实现相应的接口之前先看看其他的相关类是如何实现的是很有帮助的,它会加深你对 Sponge 的数据系统如何工作的理解。
如果你遇到困惑或在某些方面不能确认,请访问 #spongedev
IRC 频道及论坛,或在 GitHub 上创建一个问题。记得检查 Data Processor Implementation Checklist 以确定我们需要什么样的贡献。