diff --git a/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/api/tag/client/v1/ClientTags.java b/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/api/tag/client/v1/ClientTags.java index f4186746ad..23407c94fc 100644 --- a/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/api/tag/client/v1/ClientTags.java +++ b/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/api/tag/client/v1/ClientTags.java @@ -37,6 +37,9 @@ *

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. + * + *

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() { diff --git a/fabric-data-generation-api-v1/build.gradle b/fabric-data-generation-api-v1/build.gradle index 60560e7edd..07d4bf621d 100644 --- a/fabric-data-generation-api-v1/build.gradle +++ b/fabric-data-generation-api-v1/build.gradle @@ -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' ]) diff --git a/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/api/datagen/v1/provider/FabricTagProvider.java b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/api/datagen/v1/provider/FabricTagProvider.java index cac205fc07..de31526fc3 100644 --- a/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/api/datagen/v1/provider/FabricTagProvider.java +++ b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/api/datagen/v1/provider/FabricTagProvider.java @@ -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; @@ -70,6 +75,9 @@ * @see EntityTypeTagProvider */ public abstract class FabricTagProvider extends TagProvider { + private final FabricDataOutput output; + private final Map aliasGroupBuilders = new HashMap<>(); + /** * Constructs a new {@link FabricTagProvider} with the default computed path. * @@ -80,6 +88,7 @@ public abstract class FabricTagProvider extends TagProvider { */ public FabricTagProvider(FabricDataOutput output, RegistryKey> registryKey, CompletableFuture registriesFuture) { super(output, registryKey, registriesFuture); + this.output = output; } /** @@ -116,6 +125,34 @@ protected FabricTagBuilder getOrCreateTagBuilder(TagKey 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 getAliasGroupBuilders() { + return Collections.unmodifiableMap(aliasGroupBuilders); + } + /** * Extend this class to create {@link Block} tags in the "/blocks" tag directory. */ @@ -396,4 +433,52 @@ public final FabricTagBuilder add(RegistryKey... registryKeys) { return this; } } + + /** + * A builder for tag alias groups. + */ + public final class AliasGroupBuilder { + private final List> tags = new ArrayList<>(); + + private AliasGroupBuilder() { + } + + /** + * {@return a read-only list of the tags in this alias group}. + */ + public List> getTags() { + return Collections.unmodifiableList(tags); + } + + public AliasGroupBuilder add(TagKey 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... tags) { + for (TagKey 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; + } + } } diff --git a/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/impl/datagen/TagAliasGenerator.java b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/impl/datagen/TagAliasGenerator.java new file mode 100644 index 0000000000..9e818b5773 --- /dev/null +++ b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/impl/datagen/TagAliasGenerator.java @@ -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> 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 CompletableFuture writeTagAlias(DataWriter writer, DataOutput.PathResolver pathResolver, RegistryKey> registryRef, Identifier groupId, List> tags) { + Path path = pathResolver.resolveJson(groupId); + return DataProvider.writeCodecToPath(writer, TagAliasGroup.codec(registryRef), new TagAliasGroup<>(tags), path); + } +} diff --git a/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/mixin/datagen/TagProviderMixin.java b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/mixin/datagen/TagProviderMixin.java index 3ae2955117..2a1fcd0074 100644 --- a/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/mixin/datagen/TagProviderMixin.java +++ b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/mixin/datagen/TagProviderMixin.java @@ -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.server.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 { + @Shadow + @Final + protected RegistryKey> registryRef; + + @Unique + private DataOutput.PathResolver tagAliasPathResolver; + + @Inject(method = "(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> 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;(Ljava/util/List;Z)V"), index = 1) private boolean addReplaced(boolean replaced, @Local TagBuilder tagBuilder) { if (tagBuilder instanceof FabricTagBuilder fabricTagBuilder) { @@ -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 addTagAliasGroupBuilders(CompletableFuture[] futures, Operation> original, @Local(argsOnly = true) DataWriter writer) { + if ((Object) this instanceof FabricTagProvider) { + // Note: no pattern matching instanceof so that we can cast directly to FabricTagProvider instead of a wildcard + Map.AliasGroupBuilder> builders = ((FabricTagProvider) (Object) this).getAliasGroupBuilders(); + CompletableFuture[] newFutures = Arrays.copyOf(futures, futures.length + builders.size()); + int index = futures.length; + + for (Map.Entry.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); + } + } } diff --git a/fabric-data-generation-api-v1/src/main/resources/fabric.mod.json b/fabric-data-generation-api-v1/src/main/resources/fabric.mod.json index 8f1a7a6ed0..cff1a04de7 100644 --- a/fabric-data-generation-api-v1/src/main/resources/fabric.mod.json +++ b/fabric-data-generation-api-v1/src/main/resources/fabric.mod.json @@ -16,7 +16,8 @@ "FabricMC" ], "depends": { - "fabricloader": ">=0.16.8" + "fabricloader": ">=0.16.8", + "fabric-tag-api-v1": "*" }, "description": "Allows for automatic data generation.", "mixins": [ diff --git a/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/tag_aliases/block/flowers.json b/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/tag_aliases/block/flowers.json new file mode 100644 index 0000000000..69f4ed3098 --- /dev/null +++ b/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/tag_aliases/block/flowers.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "minecraft:flowers", + "minecraft:flower_pots" + ] +} \ No newline at end of file diff --git a/fabric-data-generation-api-v1/src/testmod/generated/data/other_namespace/fabric/tag_aliases/block/flowers.json b/fabric-data-generation-api-v1/src/testmod/generated/data/other_namespace/fabric/tag_aliases/block/flowers.json new file mode 100644 index 0000000000..69f4ed3098 --- /dev/null +++ b/fabric-data-generation-api-v1/src/testmod/generated/data/other_namespace/fabric/tag_aliases/block/flowers.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "minecraft:flowers", + "minecraft:flower_pots" + ] +} \ No newline at end of file diff --git a/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java b/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java index 86af5fd0e2..488cdd679e 100644 --- a/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java +++ b/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java @@ -332,6 +332,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); } } diff --git a/fabric-tag-api-v1/README.md b/fabric-tag-api-v1/README.md new file mode 100644 index 0000000000..b2b4c3dfa2 --- /dev/null +++ b/fabric-tag-api-v1/README.md @@ -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//fabric/tag_alias/` +directory. `` is the path of the registry's ID, prefixed with `/` 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. diff --git a/fabric-tag-api-v1/build.gradle b/fabric-tag-api-v1/build.gradle new file mode 100644 index 0000000000..35d162fff0 --- /dev/null +++ b/fabric-tag-api-v1/build.gradle @@ -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', +]) diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/api/tag/v1/package-info.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/api/tag/v1/package-info.java new file mode 100644 index 0000000000..1f5147f1d1 --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/api/tag/v1/package-info.java @@ -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}. + * + *

Aliasing tags

+ * Tag alias groups 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. + * + *

Tag alias groups can be defined in data packs in the {@code data//fabric/tag_alias/} + * directory. {@code } is the path of the registry's ID, prefixed with {@code /} 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//fabric/tag_alias/block/}. + * + *

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. + * + *

If multiple tag alias groups include a tag, the groups will be combined and each tag will be an alias + * for the same contents. + * + *

Tag aliases in the {@code c} namespace

+ * + *

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. + * + *

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. + * + *

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; diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/SimpleRegistryExtension.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/SimpleRegistryExtension.java new file mode 100644 index 0000000000..815a21a01d --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/SimpleRegistryExtension.java @@ -0,0 +1,22 @@ +/* + * 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.tag; + +public interface SimpleRegistryExtension { + void fabric_applyPendingTagAliases(); + void fabric_refreshTags(); +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasEnabledRegistryWrapper.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasEnabledRegistryWrapper.java new file mode 100644 index 0000000000..ee41ea66f7 --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasEnabledRegistryWrapper.java @@ -0,0 +1,30 @@ +/* + * 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.tag; + +import java.util.Map; +import java.util.Set; + +import net.minecraft.registry.tag.TagKey; + +/** + * Implemented on {@code RegistryWrapper.Impl} instances used during data loading + * to give access to the underlying registry. + */ +public interface TagAliasEnabledRegistryWrapper { + void fabric_loadTagAliases(Map, Set>> aliasGroups); +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasGroup.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasGroup.java new file mode 100644 index 0000000000..494e7f779e --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasGroup.java @@ -0,0 +1,48 @@ +/* + * 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.tag; + +import java.util.List; + +import com.mojang.serialization.Codec; + +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.tag.TagKey; + +/** + * A wrapper record for tag alias groups. + * + * @param tags the tags in the group, must be from the same registry + * @param the type of registry entries in the tags + */ +public record TagAliasGroup(List> tags) { + /** + * Creates a codec for tag alias groups in the specified registry. + * + * @param registryKey the key of the registry where the tags are from + * @param the entry type + * @return the codec + */ + public static Codec> codec(RegistryKey> registryKey) { + return TagKey.unprefixedCodec(registryKey) + .listOf() + .fieldOf("tags") + .xmap(TagAliasGroup::new, TagAliasGroup::tags) + .codec(); + } +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasLoader.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasLoader.java new file mode 100644 index 0000000000..d26e8860c2 --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagAliasLoader.java @@ -0,0 +1,177 @@ +/* + * 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.tag; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.Lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.registry.CombinedDynamicRegistries; +import net.minecraft.registry.DynamicRegistryManager; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceFinder; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.SinglePreparationResourceReloader; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; + +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; + +public final class TagAliasLoader extends SinglePreparationResourceReloader>, List>> implements IdentifiableResourceReloadListener { + public static final Identifier ID = Identifier.of("fabric-tag-api-v1", "tag_alias_groups"); + + private static final Logger LOGGER = LoggerFactory.getLogger("fabric-tag-api-v1"); + private final RegistryWrapper.WrapperLookup registries; + + public TagAliasLoader(RegistryWrapper.WrapperLookup registries) { + this.registries = registries; + } + + @Override + public Identifier getFabricId() { + return ID; + } + + @SuppressWarnings("unchecked") + @Override + protected Map>, List> prepare(ResourceManager manager, Profiler profiler) { + Map>, List> dataByRegistry = new HashMap<>(); + Iterator>> registryIterator = registries.streamAllRegistryKeys().iterator(); + + while (registryIterator.hasNext()) { + RegistryKey> registryKey = registryIterator.next(); + ResourceFinder resourceFinder = ResourceFinder.json(getDirectory(registryKey)); + + for (Map.Entry entry : resourceFinder.findResources(manager).entrySet()) { + Identifier resourcePath = entry.getKey(); + Identifier groupId = resourceFinder.toResourceId(resourcePath); + + try (Reader reader = entry.getValue().getReader()) { + JsonElement json = JsonParser.parseReader(reader); + Codec> codec = TagAliasGroup.codec((RegistryKey>) registryKey); + + switch (codec.parse(JsonOps.INSTANCE, json)) { + case DataResult.Success(TagAliasGroup group, Lifecycle unused) -> { + var data = new Data(groupId, group); + dataByRegistry.computeIfAbsent(registryKey, key -> new ArrayList<>()).add(data); + } + case DataResult.Error error -> { + LOGGER.error("[Fabric] Couldn't parse tag alias group file '{}' from '{}': {}", groupId, resourcePath, error.message()); + } + } + } catch (IOException | JsonParseException e) { + LOGGER.error("[Fabric] Couldn't parse tag alias group file '{}' from '{}'", groupId, resourcePath, e); + } + } + } + + return dataByRegistry; + } + + private static String getDirectory(RegistryKey> registryKey) { + String directory = "fabric/tag_alias/"; + Identifier registryId = registryKey.getValue(); + + if (!Identifier.DEFAULT_NAMESPACE.equals(registryId.getNamespace())) { + directory += registryId.getNamespace() + '/'; + } + + return directory + registryId.getPath(); + } + + @Override + protected void apply(Map>, List> prepared, ResourceManager manager, Profiler profiler) { + for (Map.Entry>, List> entry : prepared.entrySet()) { + Map, Set>> groupsByTag = new HashMap<>(); + + for (Data data : entry.getValue()) { + Set> group = new HashSet<>(data.group.tags()); + + for (TagKey tag : data.group.tags()) { + Set> oldGroup = groupsByTag.get(tag); + + // If there's an old group... + if (oldGroup != null) { + // ...merge all of its tags into the current group... + group.addAll(oldGroup); + + // ...and replace the recorded group of each tag in the old group with the new group. + for (TagKey other : oldGroup) { + groupsByTag.put(other, group); + } + } + + groupsByTag.put(tag, group); + } + } + + // Remove any groups of one tag, we don't need to apply them. + groupsByTag.values().removeIf(tags -> tags.size() == 1); + + RegistryWrapper.Impl wrapper = registries.getOrThrow(entry.getKey()); + + if (wrapper instanceof TagAliasEnabledRegistryWrapper aliasWrapper) { + aliasWrapper.fabric_loadTagAliases(groupsByTag); + } else { + throw new ClassCastException("[Fabric] Couldn't apply tag aliases to registry wrapper %s (%s) since it doesn't implement TagAliasEnabledRegistryWrapper" + .formatted(wrapper, entry.getKey().getValue())); + } + } + } + + public static void applyToDynamicRegistries(CombinedDynamicRegistries registries, T phase) { + Iterator> registryEntries = registries.get(phase).streamAllRegistries().iterator(); + + while (registryEntries.hasNext()) { + Registry registry = registryEntries.next().value(); + + if (registry instanceof SimpleRegistryExtension extension) { + extension.fabric_applyPendingTagAliases(); + // This is not needed in the static registry code path as the tag aliases are applied + // before the tags are refreshed. Dynamic registry loading (including tags) takes place earlier + // than the rest of a data reload, so we need to refresh the tags manually. + extension.fabric_refreshTags(); + } else { + throw new ClassCastException("[Fabric] Couldn't apply pending tag aliases to registry %s (%s) since it doesn't implement SimpleRegistryExtension" + .formatted(registry, registry.getClass().getName())); + } + } + } + + protected record Data(Identifier groupId, TagAliasGroup group) { + } +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagInit.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagInit.java new file mode 100644 index 0000000000..dbfb528ac8 --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/impl/tag/TagInit.java @@ -0,0 +1,29 @@ +/* + * 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.tag; + +import net.minecraft.resource.ResourceType; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; + +public final class TagInit implements ModInitializer { + @Override + public void onInitialize() { + ResourceManagerHelper.get(ResourceType.SERVER_DATA).registerReloadListener(TagAliasLoader.ID, TagAliasLoader::new); + } +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/DataPackContentsMixin.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/DataPackContentsMixin.java new file mode 100644 index 0000000000..c0272436dc --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/DataPackContentsMixin.java @@ -0,0 +1,60 @@ +/* + * 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.mixin.tag; + +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +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.callback.CallbackInfo; + +import net.minecraft.registry.CombinedDynamicRegistries; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.registry.ServerDynamicRegistryType; +import net.minecraft.resource.featuretoggle.FeatureSet; +import net.minecraft.server.DataPackContents; +import net.minecraft.server.command.CommandManager; + +import net.fabricmc.fabric.impl.tag.TagAliasLoader; + +/** + * Applies pending tag aliases to dynamic (including reloadable) registries. + * The priority is 999 because it must apply the injection to applyPendingTagLoads before the tag loaded lifecycle event. + */ +@Mixin(value = DataPackContents.class, priority = 999) +abstract class DataPackContentsMixin { + @Unique + private CombinedDynamicRegistries dynamicRegistriesByType; + + @Inject(method = "", at = @At("RETURN")) + private void storeDynamicRegistries(CombinedDynamicRegistries dynamicRegistries, RegistryWrapper.WrapperLookup registries, FeatureSet enabledFeatures, CommandManager.RegistrationEnvironment environment, List pendingTagLoads, int functionPermissionLevel, CallbackInfo info) { + dynamicRegistriesByType = dynamicRegistries; + } + + @Inject(method = "applyPendingTagLoads", at = @At("RETURN")) + private void applyDynamicTagAliases(CallbackInfo info) { + // Note: when using /reload, dynamic registry tag reloading goes through the same system that is also used + // for static registries. Luckily, it doesn't break anything to run the code below even in that case, + // since the map of pending tag alias groups is cleared after they're applied in the first round. + // This code also needs to run after the vanilla code so the pending tag reloads don't override + // the alias groups for dynamic registries. + TagAliasLoader.applyToDynamicRegistries(dynamicRegistriesByType, ServerDynamicRegistryType.WORLDGEN); + TagAliasLoader.applyToDynamicRegistries(dynamicRegistriesByType, ServerDynamicRegistryType.RELOADABLE); + } +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistry2Mixin.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistry2Mixin.java new file mode 100644 index 0000000000..b5d49792c0 --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistry2Mixin.java @@ -0,0 +1,44 @@ +/* + * 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.mixin.tag; + +import java.util.Map; +import java.util.Set; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.registry.tag.TagKey; + +import net.fabricmc.fabric.impl.tag.TagAliasEnabledRegistryWrapper; + +/** + * Adds tag alias support to {@code SimpleRegistry$2}, which is the wrapper used + * for (TODO: only?) static registries during world creation and world/data reloading. + */ +@Mixin(targets = "net.minecraft.registry.SimpleRegistry$2") +abstract class SimpleRegistry2Mixin implements TagAliasEnabledRegistryWrapper { + // returns SimpleRegistry.this, which implements TagAliasEnabledRegistry + @Shadow + public abstract RegistryWrapper.Impl getBase(); + + @Override + public void fabric_loadTagAliases(Map, Set>> aliasGroups) { + ((TagAliasEnabledRegistryWrapper) getBase()).fabric_loadTagAliases(aliasGroups); + } +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistry3Mixin.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistry3Mixin.java new file mode 100644 index 0000000000..dc72fc3560 --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistry3Mixin.java @@ -0,0 +1,47 @@ +/* + * 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.mixin.tag; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.registry.SimpleRegistry; + +import net.fabricmc.fabric.impl.tag.SimpleRegistryExtension; + +/** + * This is a mixin to the Registry.PendingTagLoad implementation in SimpleRegistry. + * It applies pending tag aliases to static registries when data packs are loaded + * and to dynamic registries when data packs are reloaded using the {@code /reload} command. + * (Tags run on their own data loading system separate from resource reloaders, so we need to inject them + * once the tag and resource reloads are done, which is here.) + */ +@Mixin(targets = "net.minecraft.registry.SimpleRegistry$3") +abstract class SimpleRegistry3Mixin { + @Shadow + @Final + SimpleRegistry field_53689; + + @Inject(method = "apply", at = @At(value = "INVOKE", target = "Lnet/minecraft/registry/SimpleRegistry;refreshTags()V")) + private void applyTagAliases(CallbackInfo info) { + ((SimpleRegistryExtension) field_53689).fabric_applyPendingTagAliases(); + } +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistryMixin.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistryMixin.java new file mode 100644 index 0000000000..6fcd23e7fe --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistryMixin.java @@ -0,0 +1,124 @@ +/* + * 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.mixin.tag; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.SimpleRegistry; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.entry.RegistryEntryList; +import net.minecraft.registry.tag.TagKey; + +import net.fabricmc.fabric.impl.tag.SimpleRegistryExtension; +import net.fabricmc.fabric.impl.tag.TagAliasEnabledRegistryWrapper; + +/** + * Adds tag alias support to {@code SimpleRegistry}, the primary registry implementation. + * + *

Additionally, the {@link TagAliasEnabledRegistryWrapper} implementation is for dynamic registry tag loading. + */ +@Mixin(SimpleRegistry.class) +abstract class SimpleRegistryMixin implements SimpleRegistryExtension, TagAliasEnabledRegistryWrapper { + @Unique + private static final Logger LOGGER = LoggerFactory.getLogger("fabric-tag-api-v1"); + + @Unique + private Map, Set>> pendingTagAliasGroups; + + @Shadow + @Final + private RegistryKey> key; + + @Shadow + SimpleRegistry.TagLookup tagLookup; + + @Shadow + protected abstract RegistryEntryList.Named createNamedEntryList(TagKey tag); + + @Shadow + abstract void refreshTags(); + + @Shadow + public abstract RegistryKey> getKey(); + + @Override + public void fabric_loadTagAliases(Map, Set>> aliasGroups) { + pendingTagAliasGroups = aliasGroups; + } + + @SuppressWarnings("unchecked") + @Override + public void fabric_applyPendingTagAliases() { + if (pendingTagAliasGroups == null) return; + + Set>> uniqueAliasGroups = Sets.newIdentityHashSet(); + uniqueAliasGroups.addAll(pendingTagAliasGroups.values()); + + for (Set> aliasGroup : uniqueAliasGroups) { + Set> entries = Sets.newIdentityHashSet(); + + // Fetch all entries from each tag. + for (TagKey tag : aliasGroup) { + RegistryEntryList.Named entryList = tagLookup.getOptional((TagKey) tag).orElse(null); + + if (entryList != null) { + entries.addAll(entryList.entries); + } else { + LOGGER.info("[Fabric] Creating a new empty tag {} for unknown tag used in a tag alias group in {}", tag.id(), tag.registryRef().getValue()); + Map, RegistryEntryList.Named> tagMap = ((SimpleRegistryTagLookup2Accessor) tagLookup).fabric_getTagMap(); + + if (!(tagMap instanceof HashMap)) { + // Unfreeze the backing map. + tagMap = new HashMap<>(tagMap); + ((SimpleRegistryTagLookup2Accessor) tagLookup).fabric_setTagMap(tagMap); + } + + tagMap.put((TagKey) tag, createNamedEntryList((TagKey) tag)); + } + } + + List> entriesAsList = List.copyOf(entries); + + // Replace the old entry list contents with the merged list. + for (TagKey tag : aliasGroup) { + RegistryEntryList.Named entryList = tagLookup.getOptional((TagKey) tag).orElseThrow(); + entryList.entries = entriesAsList; + } + } + + LOGGER.debug("[Fabric] Loaded {} tag alias groups for {}", uniqueAliasGroups.size(), key.getValue()); + pendingTagAliasGroups = null; + } + + @Override + public void fabric_refreshTags() { + refreshTags(); + } +} diff --git a/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistryTagLookup2Accessor.java b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistryTagLookup2Accessor.java new file mode 100644 index 0000000000..8a484ac847 --- /dev/null +++ b/fabric-tag-api-v1/src/main/java/net/fabricmc/fabric/mixin/tag/SimpleRegistryTagLookup2Accessor.java @@ -0,0 +1,36 @@ +/* + * 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.mixin.tag; + +import java.util.Map; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.registry.entry.RegistryEntryList; +import net.minecraft.registry.tag.TagKey; + +@Mixin(targets = "net.minecraft.registry.SimpleRegistry$TagLookup$2") +public interface SimpleRegistryTagLookup2Accessor { + @Accessor("field_53694") + Map, RegistryEntryList.Named> fabric_getTagMap(); + + @Accessor("field_53694") + @Mutable + void fabric_setTagMap(Map, RegistryEntryList.Named> tagMap); +} diff --git a/fabric-tag-api-v1/src/main/resources/assets/fabric-tag-api-v1/icon.png b/fabric-tag-api-v1/src/main/resources/assets/fabric-tag-api-v1/icon.png new file mode 100644 index 0000000000..2931efbf61 Binary files /dev/null and b/fabric-tag-api-v1/src/main/resources/assets/fabric-tag-api-v1/icon.png differ diff --git a/fabric-tag-api-v1/src/main/resources/fabric-tag-api-v1.accesswidener b/fabric-tag-api-v1/src/main/resources/fabric-tag-api-v1.accesswidener new file mode 100644 index 0000000000..390d96c0a6 --- /dev/null +++ b/fabric-tag-api-v1/src/main/resources/fabric-tag-api-v1.accesswidener @@ -0,0 +1,3 @@ +accessWidener v2 named +accessible class net/minecraft/registry/SimpleRegistry$TagLookup +accessible field net/minecraft/registry/entry/RegistryEntryList$Named entries Ljava/util/List; diff --git a/fabric-tag-api-v1/src/main/resources/fabric-tag-api-v1.mixins.json b/fabric-tag-api-v1/src/main/resources/fabric-tag-api-v1.mixins.json new file mode 100644 index 0000000000..c9b582b157 --- /dev/null +++ b/fabric-tag-api-v1/src/main/resources/fabric-tag-api-v1.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.mixin.tag", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "DataPackContentsMixin", + "SimpleRegistryMixin", + "SimpleRegistry2Mixin", + "SimpleRegistry3Mixin", + "SimpleRegistryTagLookup2Accessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric-tag-api-v1/src/main/resources/fabric.mod.json b/fabric-tag-api-v1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000000..2dc2520294 --- /dev/null +++ b/fabric-tag-api-v1/src/main/resources/fabric.mod.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 1, + "id": "fabric-tag-api-v1", + "name": "Fabric Tag API (v1)", + "version": "${version}", + "environment": "*", + "license": "Apache-2.0", + "icon": "assets/fabric-tag-api-v1/icon.png", + "contact": { + "homepage": "https://fabricmc.net", + "irc": "irc://irc.esper.net:6667/fabric", + "issues": "https://github.com/FabricMC/fabric/issues", + "sources": "https://github.com/FabricMC/fabric" + }, + "authors": [ + "FabricMC" + ], + "depends": { + "fabricloader": ">=0.16.8", + "fabric-api-base": "*", + "fabric-resource-loader-v0": "*" + }, + "entrypoints": { + "main": [ + "net.fabricmc.fabric.impl.tag.TagInit" + ] + }, + "description": "Hooks for working with tags.", + "mixins": [ + "fabric-tag-api-v1.mixins.json" + ], + "accessWidener": "fabric-tag-api-v1.accesswidener", + "custom": { + "fabric-api:module-lifecycle": "stable" + } +} diff --git a/fabric-tag-api-v1/src/testmod/java/net/fabricmc/fabric/test/tag/TagAliasTest.java b/fabric-tag-api-v1/src/testmod/java/net/fabricmc/fabric/test/tag/TagAliasTest.java new file mode 100644 index 0000000000..511353e14c --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/java/net/fabricmc/fabric/test/tag/TagAliasTest.java @@ -0,0 +1,148 @@ +/* + * 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.test.tag; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.item.Item; +import net.minecraft.item.Items; +import net.minecraft.loot.LootTable; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryEntryLookup; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.registry.entry.RegistryEntryList; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.Identifier; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeKeys; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.CommonLifecycleEvents; + +public final class TagAliasTest implements ModInitializer { + private static final Logger LOGGER = LoggerFactory.getLogger(TagAliasTest.class); + + // Test 1: Alias two non-empty tags + public static final TagKey GEMS = tagKey(RegistryKeys.ITEM, "gems"); + public static final TagKey EXPENSIVE_ROCKS = tagKey(RegistryKeys.ITEM, "expensive_rocks"); + + // Test 2: Alias a non-empty tag and an empty tag + public static final TagKey REDSTONE_DUSTS = tagKey(RegistryKeys.ITEM, "redstone_dusts"); + public static final TagKey REDSTONE_POWDERS = tagKey(RegistryKeys.ITEM, "redstone_powders"); + + // Test 3: Alias a non-empty tag and a missing tag + public static final TagKey BEETROOTS = tagKey(RegistryKeys.ITEM, "beetroots"); + public static final TagKey MISSING_BEETROOTS = tagKey(RegistryKeys.ITEM, "missing_beetroots"); + + // Test 4: Given tags A, B, C, make alias groups A+B and B+C. They should get merged. + public static final TagKey BRICK_BLOCKS = tagKey(RegistryKeys.BLOCK, "brick_blocks"); + public static final TagKey MORE_BRICK_BLOCKS = tagKey(RegistryKeys.BLOCK, "more_brick_blocks"); + public static final TagKey BRICKS = tagKey(RegistryKeys.BLOCK, "bricks"); + + // Test 5: Merge tags from a world generation dynamic registry + public static final TagKey CLASSIC_BIOMES = tagKey(RegistryKeys.BIOME, "classic"); + public static final TagKey TRADITIONAL_BIOMES = tagKey(RegistryKeys.BIOME, "traditional"); + + // Test 6: Merge tags from a reloadable registry + public static final TagKey NETHER_BRICKS_1 = tagKey(RegistryKeys.LOOT_TABLE, "nether_bricks_1"); + public static final TagKey NETHER_BRICKS_2 = tagKey(RegistryKeys.LOOT_TABLE, "nether_bricks_2"); + + private static TagKey tagKey(RegistryKey> registryRef, String name) { + return TagKey.of(registryRef, Identifier.of("fabric-tag-api-v1-testmod", name)); + } + + @Override + public void onInitialize() { + CommonLifecycleEvents.TAGS_LOADED.register((registries, client) -> { + LOGGER.info("Running tag alias tests on the {}...", client ? "client" : "server"); + + assertTagContent(registries, List.of(GEMS, EXPENSIVE_ROCKS), TagAliasTest::getItemKey, + Items.DIAMOND, Items.EMERALD); + assertTagContent(registries, List.of(REDSTONE_DUSTS, REDSTONE_POWDERS), TagAliasTest::getItemKey, + Items.REDSTONE); + assertTagContent(registries, List.of(BEETROOTS, MISSING_BEETROOTS), TagAliasTest::getItemKey, + Items.BEETROOT); + assertTagContent(registries, List.of(BRICK_BLOCKS, MORE_BRICK_BLOCKS, BRICKS), TagAliasTest::getBlockKey, + Blocks.BRICKS, Blocks.STONE_BRICKS, Blocks.NETHER_BRICKS, Blocks.RED_NETHER_BRICKS); + assertTagContent(registries, List.of(CLASSIC_BIOMES, TRADITIONAL_BIOMES), + BiomeKeys.PLAINS, BiomeKeys.DESERT); + + // The loot table registry isn't synced to the client. + if (!client) { + assertTagContent(registries, List.of(NETHER_BRICKS_1, NETHER_BRICKS_2), + Blocks.NETHER_BRICKS.getLootTableKey().orElseThrow(), + Blocks.RED_NETHER_BRICKS.getLootTableKey().orElseThrow()); + } + + LOGGER.info("Tag alias tests completed successfully!"); + }); + } + + private static RegistryKey getBlockKey(Block block) { + return block.getRegistryEntry().registryKey(); + } + + private static RegistryKey getItemKey(Item item) { + return item.getRegistryEntry().registryKey(); + } + + @SafeVarargs + private static void assertTagContent(RegistryWrapper.WrapperLookup registries, List> tags, Function> keyExtractor, T... expected) { + Set> keys = Arrays.stream(expected) + .map(keyExtractor) + .collect(Collectors.toSet()); + assertTagContent(registries, tags, keys); + } + + @SafeVarargs + private static void assertTagContent(RegistryWrapper.WrapperLookup registries, List> tags, RegistryKey... expected) { + assertTagContent(registries, tags, Set.of(expected)); + } + + private static void assertTagContent(RegistryWrapper.WrapperLookup registries, List> tags, Set> expected) { + RegistryEntryLookup lookup = registries.getOrThrow(tags.getFirst().registryRef()); + + for (TagKey tag : tags) { + RegistryEntryList.Named tagEntryList = lookup.getOrThrow(tag); + Set> actual = tagEntryList.entries + .stream() + .map(entry -> entry.getKey().orElseThrow()) + .collect(Collectors.toSet()); + + if (!actual.equals(expected)) { + throw new AssertionError("Expected tag %s to have contents %s, but it had %s instead" + .formatted(tag, expected, actual)); + } + } + + LOGGER.info("Tags {} / {} were successfully aliased together", tags.getFirst().registryRef().getValue(), tags.stream() + .map(TagKey::id) + .map(Identifier::toString) + .collect(Collectors.joining(", "))); + } +} diff --git a/fabric-tag-api-v1/src/testmod/resources/assets/fabric-tag-api-v1-testmod/lang/en_us.json b/fabric-tag-api-v1/src/testmod/resources/assets/fabric-tag-api-v1-testmod/lang/en_us.json new file mode 100644 index 0000000000..af3ea0f849 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/assets/fabric-tag-api-v1-testmod/lang/en_us.json @@ -0,0 +1,15 @@ +{ + "tag.block.fabric-tag-api-v1-testmod.bricks": "Bricks", + "tag.block.fabric-tag-api-v1-testmod.brick_blocks": "Brick Blocks", + "tag.block.fabric-tag-api-v1-testmod.more_brick_blocks": "More Brick Blocks", + "tag.item.fabric-tag-api-v1-testmod.beetroots": "Beetroots", + "tag.item.fabric-tag-api-v1-testmod.expensive_rocks": "Expensive Rocks", + "tag.item.fabric-tag-api-v1-testmod.gems": "Gems", + "tag.item.fabric-tag-api-v1-testmod.missing_beetroots": "Missing Beetroots", + "tag.item.fabric-tag-api-v1-testmod.redstone_dusts": "Redstone Dusts", + "tag.item.fabric-tag-api-v1-testmod.redstone_powders": "Redstone Powders", + "tag.loot_table.fabric-tag-api-v1-testmod.nether_bricks_1": "Nether Bricks 1", + "tag.loot_table.fabric-tag-api-v1-testmod.nether_bricks_2": "Nether Bricks 2", + "tag.worldgen.biome.fabric-tag-api-v1-testmod.classic": "Classic", + "tag.worldgen.biome.fabric-tag-api-v1-testmod.traditional": "Traditional" +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/block/bricks_ab.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/block/bricks_ab.json new file mode 100644 index 0000000000..669531cea5 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/block/bricks_ab.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "fabric-tag-api-v1-testmod:bricks", + "fabric-tag-api-v1-testmod:brick_blocks" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/block/bricks_bc.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/block/bricks_bc.json new file mode 100644 index 0000000000..b5c065b8d3 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/block/bricks_bc.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "fabric-tag-api-v1-testmod:brick_blocks", + "fabric-tag-api-v1-testmod:more_brick_blocks" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/beetroots.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/beetroots.json new file mode 100644 index 0000000000..b779e07a24 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/beetroots.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "fabric-tag-api-v1-testmod:beetroots", + "fabric-tag-api-v1-testmod:missing_beetroots" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/gems.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/gems.json new file mode 100644 index 0000000000..9a1ca39c6b --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/gems.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "fabric-tag-api-v1-testmod:expensive_rocks", + "fabric-tag-api-v1-testmod:gems" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/redstone_dusts.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/redstone_dusts.json new file mode 100644 index 0000000000..db710133dd --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/item/redstone_dusts.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "fabric-tag-api-v1-testmod:redstone_dusts", + "fabric-tag-api-v1-testmod:redstone_powders" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/loot_table/nether_bricks.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/loot_table/nether_bricks.json new file mode 100644 index 0000000000..66ede166ba --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/loot_table/nether_bricks.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "fabric-tag-api-v1-testmod:nether_bricks_1", + "fabric-tag-api-v1-testmod:nether_bricks_2" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/worldgen/biome/classic.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/worldgen/biome/classic.json new file mode 100644 index 0000000000..c658e8852b --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/fabric/tag_alias/worldgen/biome/classic.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "fabric-tag-api-v1-testmod:classic", + "fabric-tag-api-v1-testmod:traditional" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/brick_blocks.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/brick_blocks.json new file mode 100644 index 0000000000..39d086d8e1 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/brick_blocks.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:nether_bricks" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/bricks.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/bricks.json new file mode 100644 index 0000000000..cd4b051815 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/bricks.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "minecraft:bricks", + "minecraft:stone_bricks" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/more_brick_blocks.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/more_brick_blocks.json new file mode 100644 index 0000000000..215aa4e800 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/block/more_brick_blocks.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:red_nether_bricks" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/beetroots.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/beetroots.json new file mode 100644 index 0000000000..a3617f9e8a --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/beetroots.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:beetroot" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/expensive_rocks.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/expensive_rocks.json new file mode 100644 index 0000000000..0f3bdf92c3 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/expensive_rocks.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:emerald" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/gems.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/gems.json new file mode 100644 index 0000000000..0b7f522446 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/gems.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:diamond" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/redstone_dusts.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/redstone_dusts.json new file mode 100644 index 0000000000..ffe4ba8887 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/redstone_dusts.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:redstone" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/redstone_powders.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/redstone_powders.json new file mode 100644 index 0000000000..d275da3f43 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/item/redstone_powders.json @@ -0,0 +1,5 @@ +{ + "replace": false, + "values": [ + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/loot_table/nether_bricks_1.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/loot_table/nether_bricks_1.json new file mode 100644 index 0000000000..2e3d5ab654 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/loot_table/nether_bricks_1.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:blocks/nether_bricks" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/loot_table/nether_bricks_2.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/loot_table/nether_bricks_2.json new file mode 100644 index 0000000000..0fc36c475d --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/loot_table/nether_bricks_2.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:blocks/red_nether_bricks" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/worldgen/biome/classic.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/worldgen/biome/classic.json new file mode 100644 index 0000000000..946af1ea02 --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/worldgen/biome/classic.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:plains" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/worldgen/biome/traditional.json b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/worldgen/biome/traditional.json new file mode 100644 index 0000000000..998789efbf --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/data/fabric-tag-api-v1-testmod/tags/worldgen/biome/traditional.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "minecraft:desert" + ] +} diff --git a/fabric-tag-api-v1/src/testmod/resources/fabric.mod.json b/fabric-tag-api-v1/src/testmod/resources/fabric.mod.json new file mode 100644 index 0000000000..822e3f73be --- /dev/null +++ b/fabric-tag-api-v1/src/testmod/resources/fabric.mod.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "fabric-tag-v1-testmod", + "name": "Fabric Tag API (v1) Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-tag-api-v1": "*" + }, + "entrypoints": { + "main": [ + "net.fabricmc.fabric.test.tag.TagAliasTest" + ] + } +} diff --git a/gradle.properties b/gradle.properties index 63d79c4564..3677bf551c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,6 +54,7 @@ fabric-resource-loader-v0-version=3.0.6 fabric-screen-api-v1-version=2.0.34 fabric-screen-handler-api-v1-version=1.3.101 fabric-sound-api-v1-version=1.0.29 +fabric-tag-api-v1-version=1.0.0 fabric-transfer-api-v1-version=5.4.3 fabric-transitive-access-wideners-v1-version=6.2.0 fabric-convention-tags-v1-version=2.1.3 diff --git a/settings.gradle b/settings.gradle index 68a94ede37..8281f2d524 100644 --- a/settings.gradle +++ b/settings.gradle @@ -63,6 +63,7 @@ include 'fabric-resource-loader-v0' include 'fabric-screen-api-v1' include 'fabric-screen-handler-api-v1' include 'fabric-sound-api-v1' +include 'fabric-tag-api-v1' include 'fabric-transfer-api-v1' include 'fabric-transitive-access-wideners-v1'