数据附件
数据附件系统允许 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。