Advanced

Data Attachments

Learn how to attach persistent data to players, entities, block entities, levels and chunks.

Overview

Data attachments let you store typed custom data directly on game objects, avoiding messy caches or mixins. They are useful for persisted pieces of state that belong to a player, entity, block entity, level or chunk.

Balm exposes a common API over Fabric's and NeoForge's implementations. Data Attachments are not supported on Forge, as Forge does not provide an equivalent API.

For attaching data to items, you should use Data Components instead.

Defining the data

Create a class for your attachment. It will need a Codec if you want to have it saved.

common/src/main/java/MyPlayerData.java
public class MyPlayerData {
    public static final Codec<MyPlayerData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
            Codec.INT.fieldOf("cooldownTicks").forGetter(MyPlayerData::getCooldownTicks)
    ).apply(instance, MyPlayerData::new));

    private int cooldownTicks;

    public MyPlayerData() {
        this(0);
    }

    public MyPlayerData(int cooldownTicks) {
        this.cooldownTicks = cooldownTicks;
    }

    public int getCooldownTicks() {
        return cooldownTicks;
    }

    public void setCooldownTicks(int cooldownTicks) {
        this.cooldownTicks = cooldownTicks;
    }
}

Registering attachments

Register attachment types through BalmRegistrars.dataAttachmentTypes(). The registration returns a DataAttachmentLookup<T> that you keep around and use for reads and writes.

common/src/main/java/ModDataAttachments.java
public class ModDataAttachments {
    public final DataAttachmentLookup<MyPlayerData> playerData;

    public ModDataAttachments(BalmDataAttachmentTypeRegistrar registrar) {
        playerData = registrar
            .register("player_data", MyPlayerData.CODEC, MyPlayerData::new, true)
            .asLookup();
    }
}

Then initialize it from your mod's common initialization code.

common/src/main/java/MyMod.java
public class MyMod {
    public static final String MOD_ID = "mymod";

    private static @Nullable ModDataAttachments dataAttachments;

    public static void initialize(BalmRegistrars registrars) {
        registrars.dataAttachmentTypes(registrar -> dataAttachments = new ModDataAttachments(registrar));
    }

    public static ModDataAttachments dataAttachments() {
        return Objects.requireNonNull(dataAttachments);
    }
}

Accessing attachments

DataAttachmentLookup<T> supports players, entities, block entities, levels and chunks.

DataAttachmentLookup<MyPlayerData> lookup = MyMod.dataAttachments().playerData;

MyPlayerData data = lookup.getOrCreate(player);
data.setCooldownTicks(20 * 60);

if (lookup.has(player)) {
    MyPlayerData existing = lookup.get(player);
}

lookup.update(player, new MyPlayerData(0));
lookup.remove(player);

The same methods are available for other supported targets.

lookup.getOrCreate(entity);
lookup.getOrCreate(blockEntity);
lookup.getOrCreate(level);
lookup.getOrCreate(chunk);

Builder options

If you need more control when registering your attachment, use the builder overload.

playerData = registrar.register("player_data", builder -> builder
    .persistent(MyPlayerData.CODEC)
    .initializer(MyPlayerData::new)
    .copyOnDeath()
).asLookup();

The builder supports:

  • persistent(codec) to save and load the attachment value.
  • initializer(supplier) to create a default value for getOrCreate().
  • copyOnDeath() to copy player data when the player respawns after death.
  • networkSynchronized(streamCodec) to synchronize attachment data to clients.
  • networkSynchronized(streamCodec, predicate) to synchronize only to players that match the predicate.