實作 DataManipulators

這是一個為希望透過建立 DataManipulators 來説明 Data API 實作的貢獻者提供的指南。可以在 SpongeCommon Issue #8 中找到要實現的 DataManipulators 更新清單。

要完全實作一個 DataManipulator,必須遵循以下步驟:

  1. 實作 DataManipulator 本身

  2. 實作 ImmutableDataManipulator

當這些步驟完成後,還必須完成以下的步驟:

  1. KeyRegistry``中註冊``Key

  2. 實作 DataProcessor

  3. 為每個由 DataManipulator 表示的值實作 ValueProcessor

  4. 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 實作的介面。

建構子

在大多数情况下,实现一个数据操纵器需要有两个构造方法:

  • 第一個建構子沒有引數,而它會呼叫第二個建構子,並傳入「預設」值。

  • 第二個構造子需要傳入所有它支援的值。

第二個構造子必須

  • 呼叫 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

SupplierConsumer 都可以使用 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 中的数据时, 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 包结构。

验证方法

验证方法总是返回一个布尔值。如果 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 (如 WetDataHealthData )应该怎么移除数据,以及这个数据到底存在不存在(如 LoreData )。如果数据本应总是存在,那么相应的 remove() 方法应该总是失败,如果它可能存在可能不存在,那么相应的 remove() 方法就应该移除它。在数据可能存在可能不存在的情况下应覆写 doesDataExist() 方法。

因为一个实体生物 总是 有着生命值和最大生命值,所以 HealthData 总是存在的,也不应该支持移除。因此我们只需要返回 failNoData() 方法的返回值,同时不需要覆写 doesDataExist() 方法。

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

获取方法

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

其它方法

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

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

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 和其对应的不可变对象,以及对应的三个获取、设置、及移除方法。

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());

更多信息

Sponge 中的 Data 是一个相当抽象的概念。我们也很难给出从原版 Minecraft 类获取需要的数据的一般指示。在实现相应的接口之前先看看其他的相关类是如何实现的是很有帮助的,它会加深你对 Sponge 的数据系统如何工作的理解。

如果你遇到困惑或在某些方面不能确认,请访问 #spongedev IRC 频道及论坛,或在 GitHub 上创建一个问题。记得检查 Data Processor Implementation Checklist 以确定我们需要什么样的贡献。