====== Modify BlockEntity data ======
In the previous tutorial, we have created a [[blockentity|block entity]]. But they are too boring as they do not have any data. Therefore, we try to add some data to it, and define ways of serializing and deserializing data.
===== Serializing Data =====
If you want to store any data in your ''BlockEntity'', you will need to save and load it, or it will only be held while the ''BlockEntity'' is loaded, and the data will reset whenever you come back to it. Luckily, saving and loading is quite simple - you only need to override ''writeNbt()'' and ''readNbt()''.
''writeNbt()'' modifies the parameter ''nbt'', which should contain all of the data in your block entity. It usually does not modify the block entity object itself. The NBT is saved to the disk, and if you need to sync your block entity data with clients, also sent through packets.
In older versions, it was very important to call ''super.writeNbt'', which saves the position and id of the block entity to the nbt. Without this, any further data you try and save would be lost as it is not associated with a position and ''BlockEntityType''. However, latest versions do not require it, as they will be handled through ''createNbt()'' method.
Knowing this, the example below demonstrates saving an integer from your ''BlockEntity'' to the nbt. In the example, the integer is saved under the key ''"number"'' - you can replace this with any string, but you can only have one entry for each key in your nbt, and you will need to remember the key in order to read the data later.
public class DemoBlockEntity extends BlockEntity {
// Store the current value of the number
private int number = 0;
public DemoBlockEntity(BlockPos pos, BlockState state) {
super(TutorialBlockEntityTypes.DEMO_BLOCK, pos, state);
}
// Serialize the BlockEntity
@Override
public void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapper) {
// Save the current value of the number to the nbt
nbt.putInt("number", number);
super.writeNbt(nbt, wrapper);
}
}
In order to read the data, you will also need to override ''readNbt''. This method is the opposite of ''writeNbt'' - instead of saving your data to a ''NBTCompound'', you are given the nbt data which you saved earlier, enabling you to retrieve any data that you need. It modifies the block entity object itself, instead of the ''nbt''. To read the number we saved earlier in the nbt, see the example below.
// Deserialize the BlockEntity
@Override
public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapper) {
super.readNbt(nbt, wrapper);
number = nbt.getInt("number");
}
To get the NBT data for the block entity, call ''createNbt(registryLookup)''. It will also handle some data like position and components.
For old versions, calling ''super.readNbt()'' and ''super.writeNbt()'' was essential, because handling data such as positions is required. In old versions, if ''createNbt'' does not exist, try ''writeNbt(new NbtCompound())''.
===== Sync data from server to client =====
The data is read in the server world usually. Most data are not needed by the client, for example, your client does not need to know what's in the chest or furnace, until you open the GUI. But for some block entities, such as signs and banners, you have to inform the client of the data of the block entity, for example, for rendering.
For version 1.17.1 and below, implement ''BlockEntityClientSerializable'' from the Fabric API. This class provides the ''fromClientTag'' and ''toClientTag'' methods, which work much the same as the previously discussed ''readNbt'' and ''writeNbt'' methods, except that they are used specifically for sending to and receiving data on the client. You may simply call ''readNbt'' and ''writeNbt'' in the ''fromClientTag'' and ''toClientTag'' methods.
For version 1.18 and above, override ''toUpdatePacket'' and ''toInitialChunkDataNbt'':
@Nullable
@Override
public Packet toUpdatePacket() {
return BlockEntityUpdateS2CPacket.create(this);
}
@Override
public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registryLookup) {
return createNbt(registryLookup);
}
public class DemoBlockEntity extends BlockEntity {
public int number = 0;
public DemoBlockEntity(BlockPos pos, BlockState state) {
super(TutorialBlockEntityTypes.DEMO_BLOCK, pos, state);
}
@Override
public void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
nbt.putInt("number", number);
super.writeNbt(nbt, registryLookup);
}
@Override
public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt);
number = nbt.getInt("number", registryLookup);
}
}
Make sure the ''number'' field is **public**, as we are going to change it in the ''DemoBlock'' class. You could also make getter and setter methods, but we just expose the field here for simplicity.
===== Modifying the data =====
This gets the ''BlockEntity'' at the right-clicked the block's position and if it's of the type ''DemoBlockEntity'', increments its ''number'' field and sends a chat message to the player.
public class DemoBlock extends BlockWithEntity {
[...]
@Override
public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
if (!world.isClient){
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof DemoBlockEntity demoBlockEntity) {
demoBlockEntity.number++;
player.sendMessage(Text.literal("Number is " + demoBlockEntity.number));
demoBlockEntity.markDirty();
}
}
return ActionResult.SUCCESS;
}
}
The method ''markDirty()'' should be called whenever the block entity is modified. This will mark the position "dirty" to force the ''writeNbt'' method to be called when the world is next saved. As a general rule of thumb, simply call ''markDirty()'' whenever you modify any custom variable in your ''BlockEntity'' class, otherwise after you exit and re-enter the world, the block entity appears as if the modification had not been done.
If you want clients to know the update (for example, they may be need to render, or pick-stack without holding ''Ctrl''), add an extra line after the ''markDirty'' call, so that the client will know the block entity has been changed:
world.updateListeners(pos, state, state, 0);
===== Using data components =====
Since 1.20.5, you can also use data components to store data. This is required if you want to write the data into item stacks. You still need to write ''readNbt'' and ''writeNbt'' methods for them to save the block entities correctly.
Create a data component type in a separate class ''TutorialDataComponentTypes'' for it. More information about registering data components can be seen in [[https://docs.fabricmc.net/develop/items/custom-data-components|Fabric Docs page]].
public class TutorialDataComponentTypes {
public static final ComponentType NUMBER = register("number", builder -> builder
.codec(Codec.INT)
.packetCodec(PacketCodecs.INTEGER));
public static ComponentType register(String path, UnaryOperator> builderOperator) {
return Registry.register(Registries.DATA_COMPONENT_TYPE, Identifier.of("tutorial", path), builderOperator.apply(ComponentType.builder()).build());
}
public static void initialize() {
}
}
Remember to refer to the method in the ''ModInitializer'':
public class ExampleMod implements ModInitializer {
[...]
@Override
public static void onInitialize() {
[..]
TutorialDataComponentTypes.initialize();
}
}
And then in the block entity:
@Override
protected void readComponents(ComponentsAccess components) {
super.readComponents(components);
this.number = components.getOrDefault(ExampleMod.NUMBER, 0);
}
@Override
protected void addComponents(ComponentMap.Builder componentMapBuilder) {
super.addComponents(componentMapBuilder);
componentMapBuilder.add(ExampleMod.NUMBER, number);
}
@Override
public void removeFromCopiedStackNbt(NbtCompound nbt) {
nbt.remove("number");
}
The usage of ''removeFromCopiedStackNbt'' is, when copying item stacks, as the components are already copied, the nbts are no longer needed. If you pick stack (which means press the mouse wheel to the block write pressing ''Ctrl''), the components will be transferred to the stack. If you need to transfer the components when pick blocks even if you do not hold ''Ctrl'' (like the behavior of vanilla banner blocks), see [[blockentity_sync_itemstack]].