Skip to content

[WIP] Holder Components#5372

Draft
CallMeEchoCodes wants to merge 28 commits into
FabricMC:26.1.2from
CallMeEchoCodes:holder-components
Draft

[WIP] Holder Components#5372
CallMeEchoCodes wants to merge 28 commits into
FabricMC:26.1.2from
CallMeEchoCodes:holder-components

Conversation

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor

@CallMeEchoCodes CallMeEchoCodes commented May 4, 2026

Summary

Holder Components provide a way to attach arbitrary data to registry holders via a custom callback, or via datapack. This builds off the vanilla DataComponentInitializers system. The system also doubles as a data-driven way to add default components to items as a side-effect of piggybacking off of DataComponentInitializers.

Goals

  • Provide a more flexible way to add immutable data to holders
  • Provide convenient methods to fetch this data from holders and objects that contain them
  • Provide a way to add this immutable data using a datapack

Non-Goals

  • It is not a goal to add built-in components (such as flammability values for blocks). This may come in a separate PR to the content registry module
  • It is not a goal to allow modification of holder components at any point other than resource reload
  • It is not a goal to provide built-in interoperability with NeoForge's DataMaps
  • It is not a goal to provide built-in interoperability with Quilt's RegistryEntryAttachments

Motivation

Tags are an extremely useful feature for defining data driven behaviour. With one TagKey, all serialization logic and registry binding is done for you, and your feature is data driven.

Tags do have one main drawback however: they only have 2 states, a holder can be in a tag, or not in a tag. Sometimes, we need more than that, we need to attach data, not just a binary on-or-off. This is what holder components are for.

Description

FabricDataComponentInitializer

In vanilla Minecraft, holders already have a DataComponentMap. This is only ever used to add default components to Items however, and these components are quite hardcoded.

Despite the hardcoding of item components, component addition is done on reload. The callback used to do this in vanilla is a DataComponentInitializers.Initializer, which looks like this:

@FunctionalInterface
public interface Initializer<T> {
	void run(DataComponentMap.Builder components, Provider context, ResourceKey<T> key);
// ...
}

The Initializer is provided a builder to add components to, and when registered can request which ResourceKeys it wishes to be run for. This is an issue when implementing data driven resources, since we have to know which holders we will want to add components to ahead of time. To solve this, FabricDataComponentInitializer has been added. It provides the current registries, a ResourceManager, and a function to get a DataComponentMap.Builder for any holder it likes.

This API is open to any mod via FabricDataComponentInitializers.register(), but in most cases, mods will be able to use the other part of this PR to add their components.

Data Holder Components

This FabricDataComponentInitializer is used by the holder component API to load holder components from datapacks.
From here on, the API described is highly experimental and subject to change, or complete rework.

Each holder gets its own file, which is placed in a similar location to a tag.

That file strongly resembles a tag as well, it looks something like this:

// data/minecraft/components/block/acacia_slab.json
{
    "replace": false,
    "components": {
        "minecraft:custom_name": "Hello, world!"
    }
}

Status [IMPORTANT]

A lot of progress has been made. The API is now at bare minimum function and is technically usable, but there is still a lot to be done.

The JSON format is proving to be a real pain. What I would like to do is have each file provide a DataComponentPatch, and then apply those patches in order. This isn't possible with the current design since for some reason DataComponentMap.Builder doesn't support patch application. This could be solved by replacing the builder with a stack of patches, but this would make the API much more annoying to use and move it further away from the vanilla design. This is now implemented!

The sync packet is functional but extremely inefficient. It uses maps inside maps inside maps and encodes components as NBT. The codec is also a crime against humanity (generics are suffering). Would highly appreciate if someone could try looking into that.

The remaining tasks are:

  • Data Generation (blocked by any changes to the JSON format)
  • Tests. Lots of tests.

Future work

This PR can be used in future to make the content registries data driven.

yes i forgot namespaces, i was copying the layout from the
registrycomponentreport but it was flawed
@sylv256 sylv256 added the enhancement New feature or request label May 4, 2026
@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

i want as much feedback as i can get. this is early and can be changed a lot (like... complete rewrite level can be changed)
so please rip into my work, dont hold back :3

@CallMeEchoCodes CallMeEchoCodes added new module Pull requests that introduce new modules help wanted Extra attention is needed labels May 4, 2026
@Bawnorton
Copy link
Copy Markdown

Bawnorton commented May 4, 2026

The structure of the component definition feels a tad backwards in my opinion. I would expect the file name to be that of the holder and the content to be the component definitions:

// data/minecraft/components/block/acacia_slab.json
{
    "replace": false,
    "components": {
        "minecraft:custom_name": "Hello, world!"
    }
}

The id (of holder) -> value (of component) feels strange.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

The structure of the component definition feels a tad backwards in my opinion. I would expect the file name to be that of the holder and the content to be the component definitions:

// data/minecraft/components/block/acacia_slab.json
{
    "replace": false,
    "components": {
        "minecraft:custom_name": "Hello, world!"
    }
}

The id (of holder) -> value (of component) feels strange.

This was modeled off of tags, it was the other way originally actually

I'm still not sure which I prefer, I'll wait for other opinions

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

I should also mention: I want to be very careful not to overcomplicate the format on initial release.

Things like removals or priorities are potentially useful but I want this to get merged this century. New features can come in subsequent PRs, this one will stay small feature wise.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

I should also mention: I want to be very careful not to overcomplicate the format on initial release.

Things like removals or priorities are potentially useful but I want this to get merged this century. New features can come in subsequent PRs, this one will stay small feature wise.

That being said, if you see something that would be difficult to extend later, please call it out to avoid a headache later.

Copy link
Copy Markdown
Member

@PepperCode1 PepperCode1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...we have inherited the weird quirk of tags being placed in namespace/tags/path for vanilla registries but namespace/tags/registry_namespace/path for modded ones.

I think this is acceptable and preferable, as it is consistent with existing behavior, and it is what people expect. I do not think the behavior should change from how it currently is.

I strongly think that synchronization should be part of this PR. Many existing item components need handling on the client, or are handled only on the client, such as the item_model component. Without synchronization, using/handling modded components with these requirements would be exceedingly difficult, and modifying vanilla item components with these requirements would not work. Additionally, mods can curruently expect that the components bound to a certain holder are the same regardless of side (when in a world), so this PR should preserve this assumption.

I agree with Bawnorton about the file format. The format they proposed feels a lot more natural due to the name of the PR and the way the vanilla code works. However, there could be advantages to leaving the format closer to how tags work, as the file format affects the replace field. This needs more consideration, specifically of use cases that may need to utilize these fields, and with which format that use case is easier.

If the JSON format supports replace, then it should also support remove in the same way that #5365 does. The key prefix should be dropped as we define the entire format.

Data generation support is important, but I think should not hold up this PR. If it is difficult to add or there are disagreements that cannot be resolved about it, it should be moved to a separate PR.

Otherwise, I agree with the fundamental design of this PR.

@cputnam-a11y
Copy link
Copy Markdown
Contributor

Another thing is synchronisation: should it be part of this PR? Please debate this in the discussion thread.

As we already sync the registries, I don't think it makes sense to have a feature seemingly tied to the holders of a registry, that isn't synced with those holders

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

Synchronisation was left out as last time I tried it I struggled immensely. I'm still not sure how the API on that would work with this design (maybe an extension of DataComponentType?), but the main issue would be impl.

The vanilla regsync code is ungodly complicated.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

Synchronisation was left out as last time I tried it I struggled immensely. I'm still not sure how the API on that would work with this design (maybe an extension of DataComponentType?), but the main issue would be impl.

The vanilla regsync code is ungodly complicated.

I'd be much more willing to work on it if someone else could work out the details of how it would work.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

CallMeEchoCodes commented May 5, 2026

One more thing:

Should the /holder_component command be included in the main module? It is currently in the testmod but I could see it being incredibly useful for debugging datapacks.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

@PepperCode1 I've implemented toposorting, haven't done this before so would appreciate review. Mostly copied from resource reloaders.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

The structure of the component definition feels a tad backwards in my opinion. I would expect the file name to be that of the holder and the content to be the component definitions:

// data/minecraft/components/block/acacia_slab.json
{
    "replace": false,
    "components": {
        "minecraft:custom_name": "Hello, world!"
    }
}

The id (of holder) -> value (of component) feels strange.

I have changed the format to this

@sylv256
Copy link
Copy Markdown
Member

sylv256 commented May 5, 2026

The structure of the component definition feels a tad backwards in my opinion. I would expect the file name to be that of the holder and the content to be the component definitions:

// data/minecraft/components/block/acacia_slab.json
{
    "replace": false,
    "components": {
        "minecraft:custom_name": "Hello, world!"
    }
}

The id (of holder) -> value (of component) feels strange.

I have changed the format to this

You should use jsonc, not js.

@ChrysanthCow
Copy link
Copy Markdown
Contributor

ChrysanthCow commented May 5, 2026

Just as an fyi, you wouldn't need to implement #5365 because component patches already have removal syntax.

"components": {
    // Removes the item model component from the target holder.
    "!minecraft:item_model": {} 
}

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

Oh that's awesome! I had no idea, thanks for telling me.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

I strongly think that synchronization should be part of this PR. Many existing item components need handling on the client, or are handled only on the client, such as the item_model component. Without synchronization, using/handling modded components with these requirements would be exceedingly difficult, and modifying vanilla item components with these requirements would not work. Additionally, mods can curruently expect that the components bound to a certain holder are the same regardless of side (when in a world), so this PR should preserve this assumption.

I have had a look into implementing this and am really not sure where to start. I wanted to copy the design that tags use, but they appear to be using a packet which explicitly uses state from the ClientPacketListener, and has different behaviour depending on which phase it is sent in.

I'm not quite sure how to recreate this using payloads.

@axialeaa
Copy link
Copy Markdown

axialeaa commented May 6, 2026

The structure of the component definition feels a tad backwards in my opinion. I would expect the file name to be that of the holder and the content to be the component definitions:

// data/minecraft/components/block/acacia_slab.json
{
    "replace": false,
    "components": {
        "minecraft:custom_name": "Hello, world!"
    }
}

The id (of holder) -> value (of component) feels strange.

I agree with this.

@axialeaa
Copy link
Copy Markdown

axialeaa commented May 6, 2026

Just a thought: since the schema has changed to components/block/<block>.json, it reminds me a little of models which, to reduce boilerplating, have a parent field for inheriting common data. In the case of components/block/acacia_slab for example, perhaps one could write a template slab file of which acacia_slab and oak_slab would be children.

Alternatively (jointly?), the file name could be arbitrary and the registry entries to which the data would be applied are defined in a HolderSet.

@cputnam-a11y
Copy link
Copy Markdown
Contributor

One problem with parenting is that only one mod could successfully do it and get intuitive results

@axialeaa
Copy link
Copy Markdown

axialeaa commented May 6, 2026

One problem with parenting is that only one mod could successfully do it and get intuitive results

That's true. Maybe only worth considering the HolderSet idea, then.

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

Sync is here. Expecting this to be very buggy, not much testing and I don't have much experience with complex networking

@CallMeEchoCodes CallMeEchoCodes changed the title [PROTOTYPE, HIGHLY WIP AND SUBJECT TO CHANGE, DO NOT MERGE] Holder Components [WIP] Holder Components May 11, 2026
// this is hopefully fine since they aren't updated often
// TODO: optimize this format. im quite sure this could be smaller (ComponentTypeId could maybe be the int id?)
// Map<RegistryKey, Map<HolderId, Map<ComponentTypeId, ComponentValue>>>
Map<ResourceKey<? extends Registry<?>>, Map<Identifier, Map<Identifier, Tag>>> registryToComponents
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you say this isnt great, what exact stream codecs can we not use? Would it be cleaner to copy them, or find a way to use them with a faked out registry or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use anything that needs a RegistryFriendlyByteBuf. All of the registry deserialization must happen later (on configure end, after new registries are built)

My plan was to change from Tag to ByteBuf and potentially also use int2object instead of obj2obj since the registry intids are theoretically the same by the time we actually use the ids

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should create our own holders and bind them later?


import net.fabricmc.fabric.impl.holder.component.FabricDataComponentInitializersImpl;

public final class FabricDataComponentInitializers {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs much better docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs have not been written, that is one of the final things I need to do before this is done

CallbackInfo ci,
@Local DynamicOps<Tag> ops
) {
connection.accept(new ClientboundCustomPayloadPacket(DataComponentNetworkSerialization.serialize(ops, registries)));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the expectend behavior if the client doesnt have fabric api, or this module, or an incompatible version of this module? I think right now this will disconnect them?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think it would disconnect them, just sent useless data and then write a warning in a log

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, you might be right I'd have to double check. Either way this should not send the packet to clients that cannot handle it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tested but I am fairly sure it will just log a warning on the client.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I check if a client supports a payload?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServerPlayNetworking.canSend

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't check that in the configure task. I don't have a ServerConfigurationPacketListenerImpl, just a Consumer<Packet<?>>

private static final Logger LOGGER = LoggerFactory.getLogger(MODID);

@Override
public void onInitialize() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if there are some automated client tests that can test the syncing. let me know if you need some help.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, don't know how to do that though, help would be appreciated

Tests are the other thing I still need to do

@sylv256
Copy link
Copy Markdown
Member

sylv256 commented May 28, 2026

Will this remain on 26.1.2 and be ported during merging, or will it require a backport to 26.1.2 after this gets merged?

@modmuss50
Copy link
Copy Markdown
Member

Unless it requires significant change this can be ported up to 26.2 when merged.

Identifier.STREAM_CODEC, PackedComponentMap::id,
StreamCodec.of(
(output, value) -> {
output.writeInt(value.readableBytes());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VarInt#read and VarInt#write can be used for this stream codec

@CallMeEchoCodes
Copy link
Copy Markdown
Contributor Author

I'm fairly confident this will work on 26.2 without changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request help wanted Extra attention is needed new module Pull requests that introduce new modules

Projects

None yet

Development

Successfully merging this pull request may close these issues.