实现数据操纵器

这是一个创建数据操纵器(DataManipulator)的指南,贡献者可以通过本指南来学习如何帮助实现数据 API(Data API)。一个实现数据操纵器的更新目录可以在 SpongeCommon Issue #8 被找到。

若要完整实现一个 DataManipulator 必须遵循这些步骤:

  1. 实现 DataManipulator 本身

  2. 实现 ImmutableDataManipulator

当这些步骤完成后,下列的这些事情也要完成:

  1. KeyRegistryModule 中注册数据键(Key

  2. 实现数据处理器( DataProcessor

  3. 为每个表示 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.util.Constants;
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;

public class SpongeHealthData extends AbstractData<HealthData, ImmutableHealthData> implements HealthData {

    private double health;
    private double maxHealth;

    public SpongeHealthData() {
        this(20D, 20D);
    }

    public SpongeHealthData(double health, double maxHealth) {
        super(HealthData.class);
        checkArgument(maxHealth > 0);
        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(0)
        .maximum(this.maxHealth)
        .defaultValue(this.maxHealth)
        .actualValue(this.health)
        .build();
}

小技巧

因为 Double 实现了 Comparable ,所有我们并不需要显式指定一个 Comparable 类的实现。

如果当前值没有指定,那么调用 Value 类的 BaseValue#get() 将返回默认值。

复制和序列化

实现 DataManipulator#copy()DataManipulator#asImmutable() 两个方法并不需要做太多的工作,你只需要根据现有的数据,分别创建对应的可变的和不可变的数据操纵器副本就可以了。

DataSerializable#toContainer() 方法用于序列化。请使用 DataContainer#createNew() 作为返回值并把相关的数据放进去。一个 DataContainer 本身可以看做 DataQuery 到对应值的映射。因为每个 Key 都有对应的 DataQuery ,所有只需要直接传入相应的 Key 就可以获得对应的值了。

public DataContainer toContainer() {
    return super.toContainer()
        .set(Keys.HEALTH, this.health)
        .set(Keys.MAX_HEALTH, this.maxHealth);
}

registerGettersAndSetters() 方法

一个 DataManipulator 同时提供了方法用于根据数据键获取和设置数据。当然,我们的 AbstractData 提供了相关实现,不过我们必须告诉它我们怎么获取数据,以及获取的是什么数据。因此,我们需要在 registerGettersAndSetters() 方法的实现中为我们的每一个数据值做下面几件事:

  • 注册一个 Supplier 用于直接获取数据

  • 注册一个 Consumer 用于直接设置数据

  • 注册一个 Supplie<Value> 用于获取可变的 Value

SupplierConsumer 都可以使用 Java8 的 Lambda 表达式。

private SpongeHealthData setCurrentHealthIfValid(double value) {
    if (value >= 0 && value <= (double) Float.MAX_VALUE) {
        this.health = value;
    } else {
        throw new IllegalArgumentException("Invalid value for current health");
    }
    return this;
}

private SpongeHealthData setMaximumHealthIfValid(double value) {
    if (value >= 0 && value <= (double) Float.MAX_VALUE) {
        this.maxHealth = value;
    } else {
        throw new IllegalArgumentException("Invalid value for maximum health");
    }
    return this;
}

private void registerGettersAndSetters() {
    registerFieldGetter(Keys.HEALTH, () -> this.health);
    registerFieldSetter(Keys.HEALTH, this::setCurrentHealthIfValid);
    registerKeyValue(Keys.HEALTH, this::health);

    registerFieldGetter(Keys.MAX_HEALTH, () -> this.maxHealth);
    registerFieldSetter(Keys.MAX_HEALTH, this::setMaximumHealthIfValid);
    registerKeyValue(Keys.MAX_HEALTH, this::maxHealth);
}

作为设置值的手段, Consumer 必须在试图设置值时进行一定的检查。尤其是当有些 DataHolder 不接受负数值的时候。所以如果传入的值不合法,该 Consumer 应该直接抛出一个 IllegalArgumentException

小技巧

对应的设置值时的要求标准应与其分别的 Value 对象相一致。所以你可以试着把 this.health().set() 方法传入的值检查部分隔离成另一个方法,然后直接执行 this.health = 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() 方法。然后,在该方法里添加一行用于(创建和)注册的新代码。

import static org.spongepowered.api.data.DataQuery.of;

this.register("health", Key.builder()
        .type(TypeTokens.BOUNDED_DOUBLE_VALUE_TOKEN)
        .id("health")
        .name("Health")
        .query(of("Health"))
        .build());
this.register("max_health", 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 的实例。你需要分别设置相应的 TypeTokenid、有一定可读性的 name,和一个 DataQueryDataQuery 用于序列化。你应使用 DataQuery.of() 方法并传入一个字符串获取相应的实例。传入的字符串值和字段名称类似,不过应去除下划线并使用大写驼峰式。

4. 实现数据处理器(DataProcessor)

现在我们着手解决数据处理器( DataProcessor )。一个 DataProcessor 负责联结 DataManipulator 和原版 Minecraft 的对象。当 DataHolder 需要处理原版 Minecraft 中的数据时, DataProcessorValueProcessor 将负责解决这些最底层的事情。

对于你的类名而言,你应该使用你在 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 并不相同(如 TileEntityItemStack),你往往应该为不同的 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 (如 WetDataHealthData )应该怎么移除数据,以及这个数据到底存在不存在(如 LoreData )。如果数据本应总是存在,那么相应的 remove() 方法应该总是失败,如果它可能存在可能不存在,那么相应的 remove() 方法就应该移除它。

因为实体生物一定有生命值,所以 HealthData 总是存在的,也不应该支持移除。因此我们只需要返回 DataTransactionResult#failNoData() 方法的返回值。

@Override
public DataTransactionResult remove(DataHolder dataHolder) {
    return DataTransactionResult.failNoData();
}

获取方法

一个获取方法(Getter)从一个 DataHolder 获取数据并返回一个可选的(Optional) DataManipulatorDataProcessor 接口定义了 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() 方法将返回一个包含有额外的 healthDataOptional ,当且仅当相应的 DataContainer 存在相应的数据。

其它方法

根据实现的不同抽象父类,你可能需要实现一些其他的方法。例如, AbstractEntityDataProcessor 需要在不同的地方创建 DataManipulator ,如果它既不知道子类的 Class 实例,也不知道使用的构造方法,它就没有办法做到这一点。因此它利用了一个应该被实现为 final 的抽象方法。实现这个方法只需要创建一个含有默认值的 DataManipulator 就可以了。

如果你按照我们推荐的方式实现了你的 DataManipulator ,你只需要使用没有参数的构造方法就可以了。

@Override
protected HealthData createManipulator() {
    return new SpongeHealthData();
}

5. 实现数据值处理器(ValueProcessor)

不只是 DataManipulator 需要处理有关 DataHolder 的事情, Value 也要做类似的事。因此,你需要为每个你的 DataManipulatorKey 提供相应的至少一个 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(0D)
        .maximum(((Float) Float.MAX_VALUE).doubleValue())
        .defaultValue(20D)
        .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 >= 0 && 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 频道、我们 Discord 的 #dev 频道、论坛或 GitHub 的 Issue 联系我们。记得检查 Data Processor Implementation Checklist 以确定我们需要什么样的贡献。