Holders

From Forge Community Wiki

Holders are a component of vanilla registries representing a registrable value and/or the identifier of that value; they vaguely resemble a vanilla implementation of RegistryObjects, but with substantial semantic and implementation differences. They are used extensively by tags and datapack registries; modders that use these systems should be aware of their functions and caveats.

Datapack registry elements can have reference to HolderSets, which can use tag references to define sets of registry elements.

Holders

Properties of Holders

Holders in general have the following properties:

  • A holder may or may not have a key (a ResourceLocation/ResourceKey identifying the holder/value by name)
  • A holder may or may not have a value (e.g. a block or a biome)
  • A holder may or may not belong to a specific registry instance
  • A holder may or may not be a mutable object
  • A holder is associated with zero or more tags

Types of Holder

There are three categories of holders: direct holders, standalone reference holders, and intrusive reference holders.

Direct Holders

Direct holders always hold an unregistered value, and never hold a key. Direct holders are generally created when a datapack registry json inline-defines a value in a holder field instead of declaring a reference to another json. Direct holders are immutable records, and never have tags bound to them.

A vanilla example of direct holders being used is in density functions; density functions are often composed of holders of other density functions, but the child functions themselves don't need to be registered, and so many vanilla density functions are created with inline/direct child functions.

Direct holders should not be created for values that must always exist in the registry, or for values whose registry names must be determined at some point.

Reference Holders

Reference holders refer to registered values; they are created with either a key or a value, with the other property being bound to them later in the registration process. Reference holders belong to a specific registry instance and are aware of which registry they belong to (if multiple copies of a registry exist, as is the case for datapack registries, a reference holder is only valid for a single instance of that registry).

Reference holders are mutable and may have tags bound to them.

Standalone Reference Holders

Most holders encountered will be standalone reference holders; they are created with a key, and have a value bound to them later. Each registry has at most one standalone reference holder per key; parsing a reference holder in a datapack registry json computes a holder in the relevant registry if it doesn't already exist.

Standalone reference holders are the means by which datapack registry jsons can refer to each other. They have the following lifecycle:

  • A datapack registry json is loaded that refers to another registrable by id, e.g. "minecraft:desert"
  • A standalone reference holder is retrieved from the relevant registry via a get-or-create operation
  • When a datapack registry json is loaded and fully parsed, a holder for that json is get-or-created and the parsed value is bound to it
  • When registries freeze, the get-or-create operation can no longer create new holders. If any reference holders in the registry are not fully bound at this time (with both a key and a value), an error is raised. This will occur if a datapack registry json refers by id to another datapack registry json that does not exist, and is how these references-by-id are validated.
  • Whenever tags load, each tag file's TagKey is bound to all reference holders referred to in that tag (this modifiers the holders in-place).

When datagenerating datapack registry jsons with reference holders, any reference holders must be created by the specific registry instances used by the RegistryOps/RegistryAccess used to datagen the jsons, or they will fail to serialize.

Intrusive Reference Holders

Intrusive reference holders are created with a value and have a key bound to them later. Several static registrable types (blocks, items, fluids, entitytypes, and gameevents) create intrusive holders of themselves when constructed, which have a value bound to them once the value is registered.

Intrusive holders and the means of creating them are deprecated by mojang; this seems to be a hack to allow blocks and friends to quickly look up their registry name. Intrusive holders may be removed in future minecraft releases, and should not be used by mods (value->key lookups can be done via a method in the relevant registry, or by creating standalone holders or RegistryObjects to create name-value pairs).

HolderSets

A HolderSet is an indexable set of holders, and is the preferred means by which datapack registrables can refer to other datapack registrables. Vanilla frequently uses holdersets to wire worldgen jsons together, e.g. a structure json has a holderset field for which biomes the structure can spawn in.

Vanilla has two types of holdersets and three json formats; forge expands the holderset serializer to allow additional types of holdersets. A holderset field in a json can accept a single element, a list of elements, a tag ID, or one of forge's special formats, making datapack creation very flexible when holderset fields are used.

{
  "a_biome": "desert", // single element holderset
  "some_biomes": ["plains", "forest"], // list holderset
  "biome_tag": "#is_plains", // tag holderset
  "expanded_forge_format": // more on these later
  {
    "type": "modid:custom_holderset_type",
    // additional fields as specified by the type
  }
}

Codecs can be created for holdersets to allow holdersets to be read from jsons; however, a holderset codec must be created for each registry holdersets can be made for, e.g. Biome.LIST_OF_LISTS_CODEC is the codec for HolderSet<Biome>.

HolderSets should generally only be serialized when used as components of unsynced (server-only) datapack registry jsons, such as in worldgen features. HolderSets that are serialized without RegistryOps for registry context (including when synced to clients in synced datapack registries, which do not serialize with registry context) will be deserialized as lists of unregistered objects.

Types of HolderSets

Direct HolderSets

Direct HolderSets represent an immutable list of holders. They can be defined in json as single elements or lists of elements (the single element format simply parses as a list holderset with one element in it).

{
  "a_biome": "desert", // single element holderset
  "some_biomes": ["plains", "forest"], // list holderset
}

Named HolderSets

Named HolderSets represent a tag. Named HolderSets are mutable; they cache a list and a set of holders, and this cache is reset each time tags reload.

{
  "biome_tag": "#is_plains", // tag holderset
}

Custom HolderSet Types

Forge expands the holderset codecs to allow additional json formats. Custom holderset serializers can be registered by creating a deferred register for ForgeRegistries.Keys.HOLDER_SET_TYPES, though the builtin types provided by forge should be sufficient for most use cases.

Builtin Custom HolderSet Types

Forge provides four builtin holderset types, allowing for additional set operations and representations in datapack registry jsons. The and, or, and not types are composed of other holdersets, and are therefore potentially mutable as they may be composed of mutable tag holdersets whose values are recalculated after tags reload.

Any

The forge:any holderset type represents the set of all elements of the relevant registry.

{
  "biomes":
  {
    "type": "forge:any"
  }
}


And

The forge:and type represents an intersection of other holdersets.

{
  "biomes":
  {
    "type": "forge:and",
    "values":
    [
      // list of holdersets of any format, different formats (string, list, object) can be mixed here
      "#is_plains",
      "#is_overworld"
    ]
  }
}
Or

The forge:or type represents a union of other holdersets.

{
  "biomes":
  {
    "type": "forge:or",
    "values":
    [
      // list of holdersets of any format, different formats (string, list, object) can be mixed here
      "#is_plains",
      ["swamp", "mangrove_swamp"]
    ]
  }
}
Not

The forge:not type represents the set of all elements in the registry that do not belong to the specified holderset.

{
  "biomes":
  {
    "type": "forge:not",
    "value": "#is_plains" // holderset of any format (string, list, or object)
  }
}