Skip to content

Commit

Permalink
Add tag aliases (#4198)
Browse files Browse the repository at this point in the history
* Add tag aliases

* Document and rename tag alias internals

* Make the tag alias directory singular to match Mojang's recent style

* Add a note about tag aliases to client tag documentation

* Support missing tags in alias groups

* Support tag aliases for dynamic and reloadable registries

* TagAliasGroup: Document naming conventions for c tag alias groups

* Add tag alias test mod

* Fix inline return checkstyle

* Add test for tag alias data generation

* Fix checkstyle (again)

* Add tag translations to tag API testmod

* Uncomment accidentally commented out code

* SimpleRegistryMixin: Improve a log message

* TagAliasTest: Improve assertion messages

* Fix tag aliases for dynamic registries not applying on /reload

* Clean up log message once again

* Address review feedback

* Make missing interfaces throw CCEs

* Add README

* Move TagAliasGroup into the impl package

(cherry picked from commit a730659)
  • Loading branch information
Juuxel authored and modmuss50 committed Dec 12, 2024
1 parent b1caf1e commit 20ea1e2
Show file tree
Hide file tree
Showing 50 changed files with 1,256 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
* <p>Client Tags resolve that issue by lazily reading the tag json files within the mods on the side of the caller,
* directly, allowing for mods to query tags such as {@link net.fabricmc.fabric.api.tag.convention.v2.ConventionalBlockTags}
* even when connected to a vanilla server.
*
* <p>Note that locally read client tags don't currently support Fabric's tag aliases. The aliasing system is only
* implemented on servers.
*/
public final class ClientTags {
private ClientTags() {
Expand Down
1 change: 1 addition & 0 deletions fabric-data-generation-api-v1/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ moduleDependencies(project, [
'fabric-networking-api-v1',
'fabric-resource-conditions-api-v1',
'fabric-item-group-api-v1',
'fabric-tag-api-v1',
'fabric-recipe-api-v1'
])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

package net.fabricmc.fabric.api.datagen.v1.provider;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -70,6 +75,9 @@
* @see EntityTypeTagProvider
*/
public abstract class FabricTagProvider<T> extends TagProvider<T> {
private final FabricDataOutput output;
private final Map<Identifier, AliasGroupBuilder> aliasGroupBuilders = new HashMap<>();

/**
* Constructs a new {@link FabricTagProvider} with the default computed path.
*
Expand All @@ -80,6 +88,7 @@ public abstract class FabricTagProvider<T> extends TagProvider<T> {
*/
public FabricTagProvider(FabricDataOutput output, RegistryKey<? extends Registry<T>> registryKey, CompletableFuture<RegistryWrapper.WrapperLookup> registriesFuture) {
super(output, registryKey, registriesFuture);
this.output = output;
}

/**
Expand Down Expand Up @@ -116,6 +125,34 @@ protected FabricTagBuilder getOrCreateTagBuilder(TagKey<T> tag) {
return new FabricTagBuilder(super.getOrCreateTagBuilder(tag));
}

/**
* Gets an {@link AliasGroupBuilder} with the given ID.
*
* @param groupId the group ID
* @return the alias group builder
*/
protected AliasGroupBuilder aliasGroup(Identifier groupId) {
return aliasGroupBuilders.computeIfAbsent(groupId, key -> new AliasGroupBuilder());
}

/**
* Gets an {@link AliasGroupBuilder} with the given ID.
*
* @param group the group name
* @return the alias group builder
*/
protected AliasGroupBuilder aliasGroup(String group) {
Identifier groupId = Identifier.of(output.getModId(), group);
return aliasGroupBuilders.computeIfAbsent(groupId, key -> new AliasGroupBuilder());
}

/**
* {@return a read-only map of alias group builders by the alias group ID}.
*/
public Map<Identifier, AliasGroupBuilder> getAliasGroupBuilders() {
return Collections.unmodifiableMap(aliasGroupBuilders);
}

/**
* Extend this class to create {@link Block} tags in the "/blocks" tag directory.
*/
Expand Down Expand Up @@ -396,4 +433,52 @@ public final FabricTagBuilder add(RegistryKey<T>... registryKeys) {
return this;
}
}

/**
* A builder for tag alias groups.
*/
public final class AliasGroupBuilder {
private final List<TagKey<T>> tags = new ArrayList<>();

private AliasGroupBuilder() {
}

/**
* {@return a read-only list of the tags in this alias group}.
*/
public List<TagKey<T>> getTags() {
return Collections.unmodifiableList(tags);
}

public AliasGroupBuilder add(TagKey<T> tag) {
if (tag.registryRef() != registryRef) {
throw new IllegalArgumentException("Tag " + tag + " isn't from the registry " + registryRef);
}

this.tags.add(tag);
return this;
}

@SafeVarargs
public final AliasGroupBuilder add(TagKey<T>... tags) {
for (TagKey<T> tag : tags) {
add(tag);
}

return this;
}

public AliasGroupBuilder add(Identifier tag) {
this.tags.add(TagKey.of(registryRef, tag));
return this;
}

public AliasGroupBuilder add(Identifier... tags) {
for (Identifier tag : tags) {
this.tags.add(TagKey.of(registryRef, tag));
}

return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.datagen;

import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import net.minecraft.data.DataOutput;
import net.minecraft.data.DataProvider;
import net.minecraft.data.DataWriter;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.impl.tag.TagAliasGroup;

public final class TagAliasGenerator {
public static String getDirectory(RegistryKey<? extends Registry<?>> registryKey) {
String directory = "fabric/tag_aliases/";
Identifier registryId = registryKey.getValue();

if (!Identifier.DEFAULT_NAMESPACE.equals(registryId.getNamespace())) {
directory += registryId.getNamespace() + '/';
}

return directory + registryId.getPath();
}

public static <T> CompletableFuture<?> writeTagAlias(DataWriter writer, DataOutput.PathResolver pathResolver, RegistryKey<? extends Registry<T>> registryRef, Identifier groupId, List<TagKey<T>> tags) {
Path path = pathResolver.resolveJson(groupId);
return DataProvider.writeCodecToPath(writer, TagAliasGroup.codec(registryRef), new TagAliasGroup<>(tags), path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,48 @@

package net.fabricmc.fabric.mixin.datagen;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import net.minecraft.data.DataOutput;
import net.minecraft.data.DataWriter;
import net.minecraft.data.tag.TagProvider;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.tag.TagBuilder;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider;
import net.fabricmc.fabric.impl.datagen.FabricTagBuilder;
import net.fabricmc.fabric.impl.datagen.TagAliasGenerator;

@Mixin(TagProvider.class)
public class TagProviderMixin {
public class TagProviderMixin<T> {
@Shadow
@Final
protected RegistryKey<? extends Registry<T>> registryRef;

@Unique
private DataOutput.PathResolver tagAliasPathResolver;

@Inject(method = "<init>(Lnet/minecraft/data/DataOutput;Lnet/minecraft/registry/RegistryKey;Ljava/util/concurrent/CompletableFuture;Ljava/util/concurrent/CompletableFuture;)V", at = @At("RETURN"))
private void initPathResolver(DataOutput output, RegistryKey<? extends Registry<T>> registryRef, CompletableFuture<?> registriesFuture, CompletableFuture<?> parentTagLookupFuture, CallbackInfo info) {
tagAliasPathResolver = output.getResolver(DataOutput.OutputType.DATA_PACK, TagAliasGenerator.getDirectory(registryRef));
}

@ModifyArg(method = "method_27046", at = @At(value = "INVOKE", target = "Lnet/minecraft/registry/tag/TagFile;<init>(Ljava/util/List;Z)V"), index = 1)
private boolean addReplaced(boolean replaced, @Local TagBuilder tagBuilder) {
if (tagBuilder instanceof FabricTagBuilder fabricTagBuilder) {
Expand All @@ -36,4 +66,23 @@ private boolean addReplaced(boolean replaced, @Local TagBuilder tagBuilder) {

return replaced;
}

@SuppressWarnings("unchecked")
@WrapOperation(method = "method_49659", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;allOf([Ljava/util/concurrent/CompletableFuture;)Ljava/util/concurrent/CompletableFuture;"))
private CompletableFuture<Void> addTagAliasGroupBuilders(CompletableFuture<?>[] futures, Operation<CompletableFuture<Void>> original, @Local(argsOnly = true) DataWriter writer) {
if ((Object) this instanceof FabricTagProvider<?>) {
// Note: no pattern matching instanceof so that we can cast directly to FabricTagProvider<T> instead of a wildcard
Map<Identifier, FabricTagProvider<T>.AliasGroupBuilder> builders = ((FabricTagProvider<T>) (Object) this).getAliasGroupBuilders();
CompletableFuture<?>[] newFutures = Arrays.copyOf(futures, futures.length + builders.size());
int index = futures.length;

for (Map.Entry<Identifier, FabricTagProvider<T>.AliasGroupBuilder> entry : builders.entrySet()) {
newFutures[index++] = TagAliasGenerator.writeTagAlias(writer, tagAliasPathResolver, registryRef, entry.getKey(), entry.getValue().getTags());
}

return original.call((Object) newFutures);
} else {
return original.call((Object) futures);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"FabricMC"
],
"depends": {
"fabricloader": ">=0.16.9"
"fabricloader": ">=0.16.9",
"fabric-tag-api-v1": "*"
},
"description": "Allows for automatic data generation.",
"mixins": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tags": [
"minecraft:flowers",
"minecraft:flower_pots"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tags": [
"minecraft:flowers",
"minecraft:flower_pots"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ protected void configure(RegistryWrapper.WrapperLookup registries) {
getOrCreateTagBuilder(BlockTags.FIRE).setReplace(true).add(SIMPLE_BLOCK);
getOrCreateTagBuilder(BlockTags.DIRT).add(SIMPLE_BLOCK);
getOrCreateTagBuilder(BlockTags.ACACIA_LOGS).forceAddTag(BlockTags.ANIMALS_SPAWNABLE_ON);

aliasGroup("flowers")
.add(BlockTags.FLOWERS, BlockTags.FLOWER_POTS);
aliasGroup(Identifier.of("other_namespace", "flowers"))
.add(BlockTags.FLOWERS, BlockTags.FLOWER_POTS);
}
}

Expand Down
17 changes: 17 additions & 0 deletions fabric-tag-api-v1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Fabric Tag API (v1)

This module contains APIs for working with data pack tags.

## Tag aliases

*Tag alias groups* merge tags that refer to the same set of registry entries.
The contained tags will be linked together and get the combined set of entries
of all the aliased tags in a group.

Tag alias groups can be defined in data packs in the `data/<mod namespace>/fabric/tag_alias/<registry>`
directory. `<registry>` is the path of the registry's ID, prefixed with `<registry's namespace>/` if it's
not `minecraft`.

The JSON format of tag alias groups is an object with a `tags` list containing plain tag IDs.

See the module javadoc for more information about tag aliases.
14 changes: 14 additions & 0 deletions fabric-tag-api-v1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version = getSubprojectVersion(project)

loom {
accessWidenerPath = file('src/main/resources/fabric-tag-api-v1.accesswidener')
}

moduleDependencies(project, [
'fabric-api-base',
'fabric-resource-loader-v0'
])

testDependencies(project, [
':fabric-lifecycle-events-v1',
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* The Fabric Tag API for working with {@linkplain net.minecraft.registry.tag.TagKey tags}.
*
* <h1>Aliasing tags</h1>
* <dfn>Tag alias groups</dfn> are lists of tags that refer to the same set of registry entries.
* The contained tags will be linked together and get the combined set of entries
* of all the aliased tags in a group.
*
* <p>Tag alias groups can be defined in data packs in the {@code data/<mod namespace>/fabric/tag_alias/<registry>}
* directory. {@code <registry>} is the path of the registry's ID, prefixed with {@code <registry's namespace>/} if it's
* not {@value net.minecraft.util.Identifier#DEFAULT_NAMESPACE}. For example, an alias group for block tags would be placed
* in {@code data/<mod namespace>/fabric/tag_alias/block/}.
*
* <p>The JSON format of tag alias groups is an object with a {@code tags} list. The list contains plain tag IDs with
* no {@code #} prefix.
*
* <p>If multiple tag alias groups include a tag, the groups will be combined and each tag will be an alias
* for the same contents.
*
* <h2>Tag aliases in the {@code c} namespace</h2>
*
* <p>For the names of shared {@code c} tag alias groups, it's important that you use a short and descriptive name.
* A good way to do this is reusing the name of a contained {@code c} tag that follows the naming conventions.
* For example, if the tag alias group contains the tags {@code c:flowers/tall} and {@code minecraft:tall_flowers},
* the tag alias file should be named {@code flowers/tall.json}, like the contained {@code c} tag.
*
* <p>Tag alias groups in the {@code c} namespace are primarily intended for merging a {@code c} tag
* with an equivalent vanilla tag with no potentially unwanted gameplay behavior. If a vanilla tag affects
* game mechanics (such as the water tag affecting swimming), don't alias it as a {@code c} tag.
*
* <p>If you want to have the contents of a {@code c} tag in your own tag, prefer including the {@code c} tag
* in your tag file directly. That way, data packs can modify your tag separately. Tag aliases make their contained
* tags almost fully indistinguishable since they get the exact same content, and you have to override the alias group
* in a higher-priority data pack to unlink them.
*/
package net.fabricmc.fabric.api.tag.v1;
Loading

0 comments on commit 20ea1e2

Please sign in to comment.