Astral Realms Documentation Help

Item Data

Every ItemInstance carries a typed data bag — a Map<Key, Object> persisted alongside the item in the PDC. Built-in adapters cover XP/level progression, "remember a Material", the applied skin, and a captured entity; plugins can register their own adapters via ItemDataRegistry.

How it works

┌──────────────────────────────────────────────────────────────────┐ │ ItemInstance │ │ ┌────────────┐ ┌──────────────┐ ┌──────────┐ ┌─────────────┐ │ │ │ uniqueId │ │ blueprint │ │ version │ │ data: Map │ │ │ └────────────┘ └──────────────┘ └──────────┘ └─────────────┘ │ └──────────────────────────────────────────────────────────────────┘ │ ItemDataRegistry.findByKey(key) ◄─────────────┘ │ ▼ ItemDataAdapter<T> (serialize / deserialize binary bytes ↔ T) │ ▼ PDC sub-key "astralitems:data" ─► nested PDC
  1. A blueprint declares its default-data block — keys are Key strings (e.g. astralitems:scalable), values are deserialised by the registered adapter.

  2. ItemFactory.make(blueprint) copies the default-data entries into the new ItemInstance's data map.

  3. When the stack is serialised, ItemInstanceDataType iterates the data map and asks each adapter to write its entry into a nested PersistentDataContainer keyed by the adapter's Key.

  4. When the stack is deserialised, the same key drives ItemDataRegistry.findByKey to recover the adapter and read the bytes back into a typed object.

Each piece of data is opaque to the framework — it is only the adapter that knows how to read or write it. New adapters can be added without touching the rest of the plugin.

ItemDataAdapter contract

public interface ItemDataAdapter<T> { Key key(); T deserialize(BinaryMessage message); void serialize(BinaryMessage message, T data); default @Nullable T defaultValue() { return null; } Class<T> type(); }

Method

Purpose

key()

The Key under which the adapter is registered. Used as the PDC sub-key and as the YAML key in default-data.

deserialize(BinaryMessage)

Read a value from the binary stream. Called once per item when the stack is loaded.

serialize(BinaryMessage, T)

Write a value to the binary stream. Called when the item is mutated and saved.

defaultValue()

Optional fallback used when a blueprint's default-data value cannot be deserialised. Returning null (the default) makes such a value a hard blueprint-load error.

type()

Runtime class for type-safe lookups via ItemInstance.findDataByType.

BinaryMessage is the same primitive stream used by AstralCore's packet protocol — see Infrastructure › Messaging for the helpers (writeInt, writeEnum, writeOptional, writeUtf8, …).

Registering an adapter

ItemDataRegistry is a static @UtilityClass — register early (during plugin enable, before any blueprint that references the key is loaded):

import com.astralrealms.items.registry.ItemDataRegistry; ItemDataRegistry.registerAdapter(new MyCustomDataAdapter());

The registry uses a ConcurrentHashMap, so it is safe to register from any thread — but blueprints loaded before registration will not be able to resolve the key. Register synchronously on enable.

A minimal custom adapter

A "kill counter" that tracks how many entities a sword has killed:

public class KillCountData { private int kills; public KillCountData() { this(0); } public KillCountData(int kills) { this.kills = kills; } public int kills() { return kills; } public void kills(int kills) { this.kills = kills; } } public class KillCountDataAdapter implements ItemDataAdapter<KillCountData> { public static final Key KEY = Key.key("myplugin", "kill_count"); @Override public Key key() { return KEY; } @Override public KillCountData deserialize(BinaryMessage message) { return new KillCountData(message.readInt()); } @Override public void serialize(BinaryMessage message, KillCountData data) { message.writeInt(data.kills()); } @Override public Class<KillCountData> type() { return KillCountData.class; } }

Blueprint reference:

default-data: "myplugin:kill_count": kills: 0

Mutating from a pipeline link (example sketch — registering a custom link is not yet pluggable, but the same pattern applies inside a Bukkit listener):

plugin.items().fromItemStack(stack).ifPresent(instance -> { KillCountData data = instance.findDataByType(KillCountData.class).orElse(new KillCountData()); data.kills(data.kills() + 1); instance.setData(KillCountDataAdapter.KEY, data); plugin.items().updateItemStack(stack, instance); });

Making the data placeholder-accessible

To expose data through the placeholder system, the data class can implement ComplexPlaceholder (namespace() + get(PlaceholderContext)). The framework's built-in adapters do this — see ScalableItemData and StoredMaterialItemData. Implementations are then reachable from YAML as %instance_data_<key>_<subkey>%.

Built-in adapters

ScalableItemData

Key: astralitems:scalable

Holds a level + experience pair. Mutated by the scalable link.

public class ScalableItemData { private int level; // default 1 private int experience; // default 0 }

Placeholders (namespace scalable):

Placeholder

Resolves to

%instance_data_scalable_level%

Current level.

%instance_data_scalable_experience%

Accumulated XP since the last level-up.

%instance_data_scalable%

The data object itself (for [ifequal] requirements, etc.).

Default-data form:

default-data: "astralitems:scalable": level: 1 experience: 0

StoredMaterialItemData

Key: astralitems:stored-material

Remembers one Material. Used by set-stored-material, build-wand, and similar tools that need to "lock" a block type onto the item.

public class StoredMaterialItemData { private Material material; // default AIR }

Placeholders (namespace stored-material):

Placeholder

Resolves to

%instance_data_stored-material%

The stored Material (or AIR if unset).

%instance_data_stored-material_material%

Same as above — explicit subkey form.

%instance_data_stored-material_name%

Translatable component for the material (e.g. block.minecraft.oak_planks), or the none message when unset.

%instance_data_stored-material_sprite%

Adventure ObjectContents.sprite component pointing at the block / item texture — renders the material as an inline icon in chat/lore. Block materials use the default atlas and the key minecraft:block/<material>; non-block materials use the minecraft:items atlas and minecraft:item/<material>. Empty component when no material is stored.

NETHER_WART is special-cased in sprite: minecraft:block/nether_wart is not a real sprite, so the key is rewritten to minecraft:block/nether_wart_stage0.

Default-data form:

default-data: "astralitems:stored-material": material: AIR

SkinItemData

Key: astralitems:skin

Remembers which skin is applied to the item. Set on the ItemInstance by SkinService.apply and removed by SkinService.unapply; it only reaches the stack's PDC when the caller then runs ItemService.updateItemStack — the inventory swap and /items set-skin do, the anvil branch does not. ItemFactory.make reads it back to re-apply the item model when the stack is rebuilt.

public class SkinItemData { private String id; // skin id private Key model; // the skin's model-key }

hasSkin() is true only when both fields are non-null. The adapter writes a leading boolean: when the data is missing or incomplete only false is written, and deserialisation yields no entry.

Placeholders (namespace skin):

Placeholder

Resolves to

%instance_data_skin%

The data object itself.

%instance_data_skin_id%

The applied skin's id.

%instance_data_skin_model%

The applied skin's model Key.

%instance_data_skin_hasSkin%

true when both id and model are set. Note the camelCase sub-key.

%instance_data_skin_blueprint_*%

Chains into the SkinBlueprint resolved from the id — e.g. %instance_data_skin_blueprint_item%. Both classes use the skin namespace, hence the skin_blueprint_… double hop.

Like StoredEntityItemData, this entry is written only at runtime — there is no meaningful default-data form for it.

StoredEntityItemData

Key: astralitems:entity

Stores a captured entity as serialised bytes. Written by store-entity and cleared by release-entity — the backing data for mob-catcher / pet-wand items.

public record StoredEntityItemData(byte[] entityData, Component name) { }

name is the captured entity's translated type name (Component.translatable(EntityType)), recorded by store-entity alongside the bytes so the item can show what it is holding without deserialising the entity.

Placeholders (namespace stored-entity):

Placeholder

Resolves to

%instance_data_stored-entity%

The data object itself.

%instance_data_stored-entity_empty%

true when no entity is stored.

%instance_data_stored-entity_name%

The stored entity's translated name, or nothing when empty.

lore-pages: 0: - "<gray>Captured: <white>%instance_data_stored-entity_name%"

Unlike the other built-in data types, StoredEntityItemData is written by pipeline links at runtime rather than declared in default-data — there is no meaningful literal for a serialised entity. It does, however, declare an adapter default value (StoredEntityItemData.empty(), both fields null), so a blueprint that lists the key under default-data with a value the adapter cannot read falls back to the empty entry instead of failing to load — see Blueprints › default-data.

See also

Last modified: 03 September 2026