数据附件

数据附件

数据附件系统允许 Mod 为方块实体、区块、实体和世界附加并保存额外数据。

保存世界级额外数据时,也可以使用 SavedData。

提示 物品堆的数据附件已由原版数据组件取代。

创建附件类型

使用该系统需要注册 AttachmentType,它包含以下配置:

  • 默认值 supplier,在首次访问数据时创建实例。
  • 可选的序列化器,用于持久保存附件。
  • 配置了序列化器时,可设置 copyOnDeath,在实体死亡时自动复制数据,详见下文。

建议 如果不希望持久保存附件,就不要提供序列化器。

提供序列化器有几种方式:直接实现 IAttachmentSerializer;实现 ValueIOSerializable 并通过静态方法 AttachmentType#serializable 创建构建器;或者向构建器提供 map codec。

无论采用哪种方式,附件都必须注册到 NeoForgeRegistries.ATTACHMENT_TYPES 注册表。示例如下:

// Create the DeferredRegister for attachment types
private static final DeferredRegister<AttachmentType<?>> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.ATTACHMENT_TYPES, MOD_ID);

// Serialization via ValueIOSerializable
private static final Supplier<AttachmentType<ItemStacksResourceHandler>> HANDLER = ATTACHMENT_TYPES.register(
    "handler", () -> AttachmentType.serializable(() -> new ItemStacksResourceHandler(1)).build()
);
// Serialization via map codec
private static final Supplier<AttachmentType<Integer>> MANA = ATTACHMENT_TYPES.register(
    "mana", () -> AttachmentType.builder(() -> 0).serialize(Codec.INT.fieldOf("mana")).build()
);
// No serialization
private static final Supplier<AttachmentType<SomeCache>> SOME_CACHE = ATTACHMENT_TYPES.register(
    "some_cache", () -> AttachmentType.builder(() -> new SomeCache()).build()
);

// In your mod constructor, don't forget to register the DeferredRegister to your mod bus:
ATTACHMENT_TYPES.register(modBus);

使用附件类型

注册后,附件类型可以用于任何支持的持有对象。没有数据时调用 getData,会创建并附加一个默认实例。

// Get the ItemStacksResourceHandler if it already exists, else attach a new one:
ItemStacksResourceHandler handler = chunk.getData(HANDLER);
// Get the current player mana if it is available, else attach 0:
int playerMana = player.getData(MANA);
// And so on...

如果不希望创建默认实例,可以先通过 hasData 检查:

// Check if the chunk has the HANDLER attachment before doing anything.
if (chunk.hasData(HANDLER)) {
    ItemStacksResourceHandler handler = chunk.getData(HANDLER);
    // Do something with chunk.getData(HANDLER).
}

也可以通过 setData 更新数据:

// Increment mana by 10.
player.setData(MANA, player.getData(MANA) + 10);

重要 方块实体和区块被修改后,通常需要分别通过 setChanged 和 setUnsaved(true) 标记为脏。调用 setData 时会自动完成:

chunk.setData(MANA, chunk.getData(MANA) + 10); // will call setUnsaved automatically

但如果直接修改通过 getData 获得的数据,包括新创建的默认实例,就必须自行将方块实体或区块标记为脏:

var mana = chunk.getData(MUTABLE_MANA);
mana.set(10);
chunk.setUnsaved(true); // must be done manually because we did not use setData

向客户端同步数据

要向客户端同步方块实体、区块、世界或实体的数据附件,可以在构建器中配置 sync。通过 AttachmentHolder#getData 创建默认附件、通过 AttachmentHolder#setData 更新附件或通过 AttachmentHolder#removeData 移除附件时,都会向客户端发送同步。如果还需在其他时机发送,可以向 AttachmentHolder#syncData 传入相应 AttachmentType。

AttachmentType.Builder#sync 有三个重载,最终都创建 AttachmentSyncHandler<T>,其中 T 是附件数据类型。处理器有三个方法:网络读取 read、网络写入 write,以及判断指定玩家是否可以接收持有对象广播数据的 sendToPlayer。移除附件时不会使用同步处理器。

public class ExampleSyncHandler implements AttachmentSyncHandler<ExampleData> {

    @Override
    public void write(RegistryFriendlyByteBuf buf, ExampleData attachment, boolean initialSync) {
        // Write the attachment data to the buffer
        // If `initialSync` is true, you should write the entire attachment as the client does not have any prior data
        // If `initialSync` is false, you can choose to only write the data you would like to update
        
        // Example:
        if (initialSync) {
            // Write entire attachment
            ExampleData.STREAM_CODEC.encode(buf, attachment);
        } else {
            // Write update data
        }
    }

    @Override
    @Nullable
    public ExampleData read(IAttachmentHolder holder, RegistryFriendlyByteBuf buf, @Nullable ExampleData previousValue) {
        // Read the data from the buffer and return the new data attachment
        // `previousValue` is `null` if there was no prior data on the client
        // The result should return `null` if the data attachment should be removed

        // Example:
        if (previousValue == null) {
            // Read entire attachment
            return ExampleData.STREAM_CODEC.decode(buf);
        } else {
            // Read update data and merge to previous value
            return previousValue;
        }
    }

    @Override
    public boolean sendToPlayer(IAttachmentHolder holder, ServerPlayer to) {
        // Return whether the holder data is synced to the given player client
        // The players checked are different depending on the attachment holder:
        // - Block entities: All players tracking the chunk the block entity is within
        // - Chunk: All players tracking the chunk
        // - Entity: All players tracking the current entity, includes the current player if they are the attachment holder
        // - Level: All players in the current dimension / level

        // Example:
        // Only send the attachment if they are the attachment holder
        return holder == to;
    }
}

另外两个重载会委托给 AttachmentSyncHandler,接受用于读写的 StreamCodec,以及可选的 sendToPlayer 谓词。

// Assume ExampleData has some stream codec STREAM_CODEC

// Sync handler
public static final Supplier<AttachmentType<ExampleData>> WITH_SYNC_HANDLER = ATTACHMENT_TYPES.register(
    "with_sync_handler", () -> AttachmentType.builder(() -> new ExampleData())
        .sync(new ExampleSyncHandler())
        .build()
);

// Stream codec
public static final Supplier<AttachmentType<ExampleData>> WITH_STREAM_CODEC = ATTACHMENT_TYPES.register(
    "with_stream_codec", () -> AttachmentType.builder(() -> new ExampleData())
        .sync(ExampleData.STREAM_CODEC)
        .build()
);

// Stream codec with predicate
public static final Supplier<AttachmentType<ExampleData>> WITH_PREDICATE = ATTACHMENT_TYPES.register(
    "with_predicate", () -> AttachmentType.builder(() -> new ExampleData())
        .sync((holder, to) -> holder == to, ExampleData.STREAM_CODEC)
        .build()
);

提示 使用 StreamCodec 重载意味着每次都同步整个附件,忽略客户端之前已有的数据。

玩家死亡时复制数据

默认情况下,玩家死亡时不会复制实体数据附件。要自动复制,在附件构建器中设置 copyOnDeath。

更复杂的处理可以通过 PlayerEvent.Clone 实现,从原实体读取数据,再赋给新实体。此事件中可通过 #isWasDeath 区分死亡重生和从末地返回。这一点很重要,因为从末地返回时数据已经存在,应避免重复累加或复制。

例如:

@SubscribeEvent // on the game event bus
public static void onClone(PlayerEvent.Clone event) {
    if (event.isWasDeath() && event.getOriginal().hasData(MY_DATA)) {
        event.getEntity().getData(MY_DATA).fieldToCopy = event.getOriginal().getData(MY_DATA).fieldToCopy;
    }
}

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。