實作 DataManipulators
這是一個為希望透過建立 DataManipulators 來説明 Data API 實作的貢獻者提供的指南。可以在 SpongeCommon Issue #8 中找到要實現的 DataManipulators 更新清單。
要完全實作一個 DataManipulator
,必須遵循以下步驟:
實作
DataManipulator
本身實作
ImmutableDataManipulator
當這些步驟完成後,還必須完成以下的步驟:
在
KeyRegistry``中註冊``Key
實作
DataProcessor
為每個由
DataManipulator
表示的值實作ValueProcessor
在
SpongeSerializationRegistry
中註冊的所有內容
若有數據適用於某個塊,則還必須將多個方法混合到該塊中。
備註
請確保您遵循我們的 貢獻指南。
1. 實作 DataManipulator
DataManipulator
實作的命名原則是把它們加上 「Sponge」 前綴。也就是說為了實作 HealthData
介面,我們在適當的 package 建立一個名為 SpongeHealthData
的 class。為了實作 DataManipulator
的一部分內容,我們先從 org.spongepowered.common.data.manipulator.mutable.common
package 中新建了一個合適的抽象 class。最常用的一般是 AbstractData
,不過也針對一些特殊情況建立了抽象的 class,可以大大減少不必要的程式碼,例如只包含一個值的 DataManipulator
。
public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {
[...]
}
AbstractData class 有兩個類型的引數。第一個是由這個 class 實作的介面,第二個是由對應的 ImmutableDataManipulator
實作的介面。
建構子
大部分的情況下,實作一個抽象的 DataManipulator
會需要有兩個建構子:
第一個建構子沒有引數,而它會呼叫第二個建構子,並傳入「預設」值。
第二個構造子需要傳入所有它支援的值。
第二個構造子必須
呼叫
AbstractData
建構子,為被實作的介面傳遞 class 參照。要確保傳遞的值都是有效的
呼叫
registerGettersAndSetters()
方法
import static com.google.common.base.Preconditions.checkArgument;
public class SpongeHealthData {
public SpongeHealthData() {
this(DataConstants.DEFAULT_HEALTH, DataConstants.DEFAULT_HEALTH);
}
public SpongeHealthData(double currentHealth, double maxHealth) {
super(HealthData.class);
checkArgument(currentHealth >= DataConstants.MINIMUM_HEALTH && currentHealth <= (double) Float.MAX_VALUE);
checkArgument(maxHealth >= DataConstants.MINIMUM_HEALTH && maxHealth <= (double) Float.MAX_VALUE);
this.currentHealth = currentHealth;
this.maximumHealth = maxHealth;
this.registerGettersAndSetters();
}
...
}
因为我们知道,不管是生命值,还是最大生命值,都有着上下限,所以我们要确保传入的值不会超出上下限。我们可以使用 Guava 的 Preconditions
类,并把其中的若干方法以静态方式导入(译者注:import static)。
備註
永远不要在你的代码中使用魔数(Magic Number,如任意数字、布尔值等)。请使用 org.spongepowered.common.data.util.DataConstants
中提供的常数——或者在需要的时候自己创建一个。
介面定義的存取器
我们实现的接口定义了若干个方法用于访问数据值( Value
)对象。对于 HealthData
来说,我们有 health()
和 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
,所以我們不需要明確指定一個comparator。
如果沒有指定當前值,則呼叫 Value
上的 get()
回傳預設值。
複製和序列化
实现 copy()
和 asImmutable()
两个方法并不需要做太多的工作,你只需要根据现有的数据,分别创建对应的可变的和不可变的数据操纵器副本就可以了。
toContainer()
方法用于序列化。请使用 MemoryDataContainer
作为返回值并把相关的数据放进去。一个 DataContainer
本身可以看做 DataQuery
到对应值的映射。因为每个 Key
都有对应的 DataQuery
,所有只需要直接传入相应的 Key
就可以获得对应的值了。
public DataContainer toContainer() {
return new MemoryDataContainer()
.set(Keys.HEALTH, this.currentHealth)
.set(Keys.MAX_HEALTH, this.maximumHealth);
}
registerGettersAndSetters()
方法
一个 DataManipulator
同时提供了方法用于根据数据键获取和设置数据。当然,我们的 AbstractData
提供了相关实现,不过我们必须告诉它我们怎么获取数据,以及获取的是什么数据。因此,我们需要在 registerGettersAndSetters()
方法的实现中为我们的每一个数据值做下面几件事:
注册一个
Supplier
用于直接获取数据注册一个
Consumer
用于直接设置数据注册一个
Supplie<Value>
用于获取可变的Value
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, () -> SpongeHealthData.this.currentHealth);
registerFieldSetter(Keys.HEALTH, SpongeHealthData.this::setCurrentHealthIfValid);
registerKeyValue(Keys.HEALTH, SpongeHealthData.this::health);
registerFieldGetter(Keys.MAX_HEALTH, () -> SpongeHealthData.this.maximumHealth);
registerFieldSetter(Keys.MAX_HEALTH, SpongeHealthData.this::setMaximumHealthIfValid);
registerKeyValue(Keys.MAX_HEALTH, SpongeHealthData.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. 注册数据键(Key)
下一步是把你的 Key
都注册到 KeyRegistry
中。请寻找 org.spongepowered.common.data.key.KeyRegistry
类中的 generateKeyMap()
静态方法,然后在其中添加一行代码,把你的 Key
注册(并创建)进去。
keyMap.put("health"), makeSingleKey(Double.class, MutableBoundedValue.class, of("Health")));
keyMap.put("max_health", makeSingleKey(Double.class, MutableBoundedValue.class, of("MaxHealth")));
keyMap
把字符串映射到 Key
中,其使用的字符串应该是 Keys
类中提供的常量名的小写形式,而 Key
本身是 KeyFactory
的一个静态方法,大多数情况下是 makeSingleKey
方法产生的。这个方法首先需要传入一个对应数据的 Class 对象,这里我们是 Double
,然后需要传入一个对应 Value
的 Class 对象。该方法的第三个参数是用于序列化的 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 包结构。
验证方法
验证方法总是返回一个布尔值。如果 supports()
方法被调用,相应的验证方法就应该进行一系列检查,以确定该 DataProcessor
是否支持该种数据。
对于我们的 HealthDataProcessor
来说, supports()
方法已经被 AbstractEntityDataProcessor
实现了。默认情况下,它会根据 super()
构造方法传入的 Class 对象判断传入的参数是否是该 Class 对象对应的类的子类并在是时返回 true。
相反,我们需要提供一个名为 doesDataExist()
的方法。因为我们使用的抽象类并不知道如何获取到数据,所以我们需要实现它定义的这么一个方法。正如该方法名称所述,这个方法检查数据是否在支持的数据访问器上存在。对于 HealthDataProcessor
而言,这个方法总是返回 true,因为每一种实体生物都有生命值和最大生命值。
protected boolean doesDataExist(EntityLivingBase entity) {
return true;
}
设置方法
一个设置方法(Setter)接收一个 DataHolder
和若干需要使用的数据(如果有的话)作为参数。
DataProcessor
接口定义了一个名为 set()
的方法。该方法需要一个 DataHolder
和一个 DataManipulator
并返回一个 DataTransactionResult
。根据不同的抽象类实现,你可能不需要实现这一方法,因为抽象类已经帮你实现好了。
在这种情况下, AbstractEntityDataProcessor
解决了大部分的问题,并只需要一个方法用于设置数据,同时在设置成功时返回 true
否则返回 false
。所有关于 DataHolder
是否支持该 Data
的问题该抽象类都已解决,同时该抽象类只是创建了一个把 DataManipulator
的每个 Key
和其值相对应的映射,并由此创建一个 DataTransactionResult
,该 DataTransactionResult
决定操作是否成功。
protected boolean set(EntityLivingBase entity, Map<Key<?>, Object> keyValues) {
entity.getEntityAttribute(SharedMonsterAttributes.maxHealth)
.setBaseValue(((Double) keyValues.get(Keys.MAX_HEALTH)).floatValue());
entity.setHealth(((Double) keyValues.get(Keys.HEALTH)).floatValue());
return true;
}
小訣竅
要了解 DataTransactionResult
,请点击 对应的文档页面 ,以及参考 DataTransactionResult.Builder 的相关文档以了解如何创建。
警告
当你进行 ItemStack
的相关操作时,你可能尤其需要处理有关于 NBTTagCompound
的数据。很多的 NBT 标签名称已经在 org.spongepowered.common.data.util.NbtDataUtil
类中给出定义了,如果你想要的名称不存在,你应该在其中添加上你想要的名称,以防止代码中嵌入的硬编码。
移除方法
remove()
方法用于把 DataHolder
中的对应数据移除并返回一个 DataTransactionResult
。
我们的任何 DataProcessor
的抽象类实现都没有去实现移除方法,因为我们也不知道相应的 DataHolder
(如 WetData
或 HealthData
)应该怎么移除数据,以及这个数据到底存在不存在(如 LoreData
)。如果数据本应总是存在,那么相应的 remove()
方法应该总是失败,如果它可能存在可能不存在,那么相应的 remove()
方法就应该移除它。在数据可能存在可能不存在的情况下应覆写 doesDataExist()
方法。
因为一个实体生物 总是 有着生命值和最大生命值,所以 HealthData
总是存在的,也不应该支持移除。因此我们只需要返回 failNoData()
方法的返回值,同时不需要覆写 doesDataExist()
方法。
public DataTransactionResult remove(DataHolder dataHolder) {
return DataTransactionBuilder.failNoData();
}
获取方法
一个获取方法(Getter)从一个 DataHolder
获取数据并返回一个可选的(Optional) DataManipulator
。 DataProcessor
接口定义了 from()
和 createFrom()
方法,它们两者的区别在于前者会在数据访问器支持该类型的数据,但数据不存在时返回 Optional.empty()
,而后者会在不存在的情况下提供 DataManipulator
的默认值。
再一次地,我们的 AbstractEntityDataProcessor
会提供相关的大部分实现,并仅仅需要实现获取 DataHolder
的实际值的方法。这个方法会仅在 supports()
和 doesDataExist()
两个方法都返回 true 时调用,也就是说调用时已经假定数据存在了。
警告
如果相应的 DataHolder
不存在数据,如调用 remove()
成功(见上),那么你有必要覆写 doesDataExist()
方法,并在数据存在时返回 true
否则返回 false
。
protected Map<Key<?>, ?> getValues(EntityLivingBase entity) {
final double health = entity.getHealth();
final double maxHealth = entity.getMaxHealth();
return ImmutableMap.<Key<?>, Object>of(Keys.HEALTH, health, Keys.MAX_HEALTH, maxHealth);
}
過濾方法
一个填充方法(Filler)和获取方法不同,因为它接收一个 DataManipulator
用于填充数据。这种数据可能由 DataHolder
提供,也可能由 DataContainer
反序列化得到。这一方法将在该 DataHolder
不支持该种数据时返回 Optional.empty()
。
我们的 AbstractEntityDataProcessor
同样通过从数据访问器创建 DataManipulator
并与已有数据操纵器合并的方式实现了相应的填充方法,不过我们可没有办法帮你包办 DataContainer
的反序列化操作。
public Optional<HealthData> fill(DataContainer container, HealthData healthData) {
final Optional<Double> health = container.getDouble(Keys.HEALTH.getQuery());
final Optional<Double> maxHealth = container.getDouble(Keys.MAX_HEALTH.getQuery());
if (health.isPresent() && maxHealth.isPresent()) {
healthData.set(Keys.HEALTH, health.get());
healthData.set(Keys.MAX_HEALTH, maxHealth.get());
return Optional.of(healthData);
}
return Optional.empty();
}
fill()
方法将返回一个包含有额外的 healthData
的 Optional
,当且仅当相应的 DataContainer
存在相应的数据。
其它方法
根据实现的不同抽象父类,你可能需要实现一些其他的方法。例如, AbstractEntityDataProcessor
需要在不同的地方创建 DataManipulator
,如果它既不知道子类的 Class 实例,也不知道使用的构造方法,它就没有办法做到这一点。因此它利用了一个应该被实现为 final 的抽象方法。实现这个方法只需要创建一个含有默认值的 DataManipulator
就可以了。
如果你按照我们推荐的方式实现了你的 DataManipulator
,你只需要使用没有参数的构造方法就可以了。
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
和其对应的不可变对象,以及对应的三个获取、设置、及移除方法。
protected MutableBoundedValue<Double> constructValue(Double value) {
return SpongeValueFactory.boundedBuilder(Keys.HEALTH)
.minimum(DataConstants.MINIMUM_HEALTH)
.maximum((double) Float.MAX_VALUE)
.defaultValue(DataConstants.DEFAULT_HEALTH)
.actualValue(value)
.build();
}
protected ImmutableValue<Double> constructImmutableValue(Double value) {
return constructValue(value).asImmutable();
}
protected Optional<Double> getVal(EntityLivingBase container) {
return Optional.of((double) container.getHealth());
}
因为不存在不拥有生命值和最大生命值的 EntityLivingBase
,所以这个方法也永远不会返回一个 Optional.empty()
。
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()
方法将返回一个布尔值,用于指示相应的值是否已被成功设置。相应的实现会对越界的值进行检查,我们之前在设计数据值的构造方法时也这么做过。
public DataTransactionResult removeFrom(ValueContainer<?> container) {
return DataTransactionBuilder.failNoData();
}
因为数据总是存在,所以对其的移除操作总是失败的。
6. 注册处理器
为使 Sponge 可以使用我们的数据操纵器、数据处理器等,我们需要注册它们。 org.spongepowered.common.data.SpongeSerializationRegistry
类负责的就是这个。其中的 setupSerialization
方法有两大块用于注册,我们也应把注册的工作添加于此处。
数据处理器
一个 DataProcessor
和它对应的接口及和 DataManipulator
对应的实现类一起注册。对于任何一对可变和不可变的 DataManipulator
,相应的 DataProcessor
必须至少注册一个。
dataRegistry.registerDataProcessorAndImpl(HealthData.class, SpongeHealthData.class,
ImmutableHealthData.class, ImmutableSpongeHealthData.class,
new HealthDataProcessor());
数据值处理器
数据值处理器以完全相同的方法底部注册。对于每一个 Key
都会有一串对于 registerValueProcessor()
方法的调用以注册这些数据值处理吕。
dataRegistry.registerValueProcessor(Keys.HEALTH, new HealthValueProcessor());
dataRegistry.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 以确定我们需要什么样的贡献。