[WIP] Holder Components#5372
Conversation
yes i forgot namespaces, i was copying the layout from the registrycomponentreport but it was flawed
|
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) |
|
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 |
|
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. |
PepperCode1
left a comment
There was a problem hiding this comment.
...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.
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 |
|
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 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. |
|
One more thing: Should the |
|
@PepperCode1 I've implemented toposorting, haven't done this before so would appreciate review. Mostly copied from resource reloaders. |
I have changed the format to this |
You should use |
|
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": {}
} |
|
Oh that's awesome! I had no idea, thanks for telling me. |
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 I'm not quite sure how to recreate this using payloads. |
I agree with this. |
|
Just a thought: since the schema has changed to Alternatively (jointly?), the file name could be arbitrary and the registry entries to which the data would be applied are defined in a HolderSet. |
|
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. |
|
Sync is here. Expecting this to be very buggy, not much testing and I don't have much experience with complex networking |
| // 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Maybe we should create our own holders and bind them later?
|
|
||
| import net.fabricmc.fabric.impl.holder.component.FabricDataComponentInitializersImpl; | ||
|
|
||
| public final class FabricDataComponentInitializers { |
There was a problem hiding this comment.
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))); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Don't think it would disconnect them, just sent useless data and then write a warning in a log
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Haven't tested but I am fairly sure it will just log a warning on the client.
There was a problem hiding this comment.
How would I check if a client supports a payload?
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
Would be great if there are some automated client tests that can test the syncing. let me know if you need some help.
There was a problem hiding this comment.
Agreed, don't know how to do that though, help would be appreciated
Tests are the other thing I still need to do
|
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? |
|
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()); |
There was a problem hiding this comment.
VarInt#read and VarInt#write can be used for this stream codec
|
I'm fairly confident this will work on 26.2 without changes. |
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
DataComponentInitializerssystem. The system also doubles as a data-driven way to add default components to items as a side-effect of piggybacking off ofDataComponentInitializers.Goals
Non-Goals
DataMapsRegistryEntryAttachmentsMotivation
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: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,FabricDataComponentInitializerhas been added. It provides the current registries, aResourceManager, and a function to get aDataComponentMap.Builderfor 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
FabricDataComponentInitializeris 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:
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 aThis is now implemented!DataComponentPatch, and then apply those patches in order. This isn't possible with the current design since for some reasonDataComponentMap.Builderdoesn'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.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:
Future work
This PR can be used in future to make the content registries data driven.