tutorial:blockentity_modify_data

This is an old revision of the document!


Modify BlockEntity data

In the previous tutorial, we have created a 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. It is 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 will be lost as it is not associated with a position and BlockEntityType.

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.

DemoBlockEntity.java
public class DemoBlockEntity extends BlockEntity {
 
    // Store the current value of the number
    private int number = 7;
 
    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. As with writeNbt, it is essential that you call super.readNbt, and you will need to use the same keys to retrieve data that you saved. 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");
    }

Once you have implemented the writeNbt and readNbt methods, you simply need to ensure that they are called when needed. Whenever your block entity is modified and needs to be saved, call markDirty(). This will force the writeNbt method to be called when the world is next saved by marking the chunk in which your block is as dirty. 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.

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<ClientPlayPacketListener> toUpdatePacket() {
    return BlockEntityUpdateS2CPacket.create(this);
  }
 
  @Override
  public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registryLookup) {
    return createNbt(registryLookup);
  }

Warning: Need to call world.updateListeners(pos, state, state, Block.NOTIFY_LISTENERS); to trigger the update, otherwise the client does not know that the block entity has been changed.

DemoBlockEntity.class
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.

DemoBlock.class
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));
            }
        }
 
        return ActionResult.success(world.isClient);
    }
}

Using data components

Since 1.20.5, you can also use data components to store data. In this case, as data components have codecs that can serialize and deserialize themselves, you do not need to write readNbt and writeNbt methods for them.

If you want to have a try, remove the readNbt and writeNbt methods before. And then create a data component type in a separate class TutorialDataComponentTypes for it. More information about registering data components can be seen in Fabric Docs page.

TutorialDataComponentTypes.java
public class TutorialDataComponentTypes {
  public static final ComponentType<Integer> NUMBER = register("number", builder -> builder
      .codec(Codec.INT)
      .packetCodec(PacketCodecs.INTEGER));
 
  public static <T> ComponentType<T> register(String path, UnaryOperator<ComponentType.Builder<T>> 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:

ExampleMod.java
public class ExampleMod implements ModInitializer {
  [...]
 
  @Override
  public static void onInitialize() {
    [..]
    TutorialDataComponentTypes.initialize();
  }
}

And then in the block entity:

DemoBlockEntity.java
  // removed the `readNbt` and `writeNbt` methods.
 
  @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);
  }

Now these components can also be stored normally. If you pick stack (which means press the mouse wheel to the block write pressing Ctrl), the components will be transferred to the stack.

In some cases, you can also retain the readNbt and writeNbt methods, and override removeFromCopiedStackNbt method to remove the "number" field from the nbt.

tutorial/blockentity_modify_data.1724637858.txt.gz · Last modified: 2024/08/26 02:04 by solidblock