diff --git a/fabric-client-tags-api-v1/build.gradle b/fabric-client-tags-api-v1/build.gradle new file mode 100644 index 000000000..db40cf109 --- /dev/null +++ b/fabric-client-tags-api-v1/build.gradle @@ -0,0 +1,11 @@ +archivesBaseName = "fabric-client-tags-api-v1" +version = getSubprojectVersion(project) + +moduleDependencies(project, [ + 'fabric-api-base' +]) + +testDependencies(project, [ + ':fabric-convention-tags-v1', + ':fabric-lifecycle-events-v1', +]) 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 new file mode 100644 index 000000000..cd98f012f --- /dev/null +++ b/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/api/tag/client/v1/ClientTags.java @@ -0,0 +1,180 @@ +/* + * 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.api.tag.client.v1; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.tag.TagKey; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; +import net.minecraft.util.registry.RegistryEntry; +import net.minecraft.util.registry.RegistryKey; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.impl.tag.client.ClientTagsLoader; + +/** + * Allows the use of tags by directly loading them from the installed mods. + * + * <p>Tags are loaded by the server, either the internal server in singleplayer or the connected server and + * synced to the client. This can be a pain point for interoperability, as a tag that does not exist on the server + * because it is part of a mod only present on the client will no longer be available to the client that may wish to + * query it. + * + * <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.v1.ConventionalBlockTags} + * even when connected to a vanilla server. + */ +@Environment(EnvType.CLIENT) +public final class ClientTags { + private static final Map<TagKey<?>, Set<Identifier>> LOCAL_TAG_CACHE = new ConcurrentHashMap<>(); + + private ClientTags() { + } + + /** + * Loads a tag into the cache, recursively loading any contained tags along with it. + * + * @param tagKey the {@code TagKey} to load + * @return a set of {@code Identifier}s this tag contains + */ + public static Set<Identifier> getOrCreateLocalTag(TagKey<?> tagKey) { + Set<Identifier> ids = LOCAL_TAG_CACHE.get(tagKey); + + if (ids == null) { + ids = ClientTagsLoader.loadTag(tagKey); + LOCAL_TAG_CACHE.put(tagKey, ids); + } + + return ids; + } + + /** + * Checks if an entry is in a tag. + * + * <p>If the synced tag does exist, it is queried. If it does not exist, + * the tag populated from the available mods is checked. + * + * @param tagKey the {@code TagKey} to being checked + * @param entry the entry to check + * @return if the entry is in the given tag + */ + @SuppressWarnings("unchecked") + public static <T> boolean isInWithLocalFallback(TagKey<T> tagKey, T entry) { + Objects.requireNonNull(tagKey); + Objects.requireNonNull(entry); + + Optional<? extends Registry<?>> maybeRegistry = getRegistry(tagKey); + + if (maybeRegistry.isEmpty()) { + return false; + } + + if (!tagKey.isOf(maybeRegistry.get().getKey())) { + return false; + } + + Registry<T> registry = (Registry<T>) maybeRegistry.get(); + + Optional<RegistryKey<T>> maybeKey = registry.getKey(entry); + + // Check synced tag + if (registry.containsTag(tagKey)) { + return maybeKey.filter(registryKey -> registry.entryOf(registryKey).isIn(tagKey)) + .isPresent(); + } + + // Check local tags + Set<Identifier> ids = getOrCreateLocalTag(tagKey); + return maybeKey.filter(registryKey -> ids.contains(registryKey.getValue())).isPresent(); + } + + /** + * Checks if an entry is in a tag, for use with entries from a dynamic registry, + * such as {@link net.minecraft.world.biome.Biome}s. + * + * <p>If the synced tag does exist, it is queried. If it does not exist, + * the tag populated from the available mods is checked. + * + * @param tagKey the {@code TagKey} to be checked + * @param registryEntry the entry to check + * @return if the entry is in the given tag + */ + public static <T> boolean isInWithLocalFallback(TagKey<T> tagKey, RegistryEntry<T> registryEntry) { + Objects.requireNonNull(tagKey); + Objects.requireNonNull(registryEntry); + + // Check if the tag exists in the dynamic registry first + Optional<? extends Registry<T>> maybeRegistry = getRegistry(tagKey); + + if (maybeRegistry.isPresent()) { + if (maybeRegistry.get().containsTag(tagKey)) { + return registryEntry.isIn(tagKey); + } + } + + if (registryEntry.getKey().isPresent()) { + return isInLocal(tagKey, registryEntry.getKey().get()); + } + + return false; + } + + /** + * Checks if an entry is in a tag provided by the available mods. + * + * @param tagKey the {@code TagKey} to being checked + * @param registryKey the entry to check + * @return if the entry is in the given tag + */ + public static <T> boolean isInLocal(TagKey<T> tagKey, RegistryKey<T> registryKey) { + Objects.requireNonNull(tagKey); + Objects.requireNonNull(registryKey); + + if (tagKey.registry().getValue().equals(registryKey.getRegistry())) { + // Check local tags + Set<Identifier> ids = getOrCreateLocalTag(tagKey); + return ids.contains(registryKey.getValue()); + } + + return false; + } + + @SuppressWarnings("unchecked") + private static <T> Optional<? extends Registry<T>> getRegistry(TagKey<T> tagKey) { + Objects.requireNonNull(tagKey); + + // Check if the tag represents a dynamic registry + if (MinecraftClient.getInstance() != null) { + if (MinecraftClient.getInstance().world != null) { + if (MinecraftClient.getInstance().world.getRegistryManager() != null) { + Optional<? extends Registry<T>> maybeRegistry = MinecraftClient.getInstance().world + .getRegistryManager().getOptional(tagKey.registry()); + if (maybeRegistry.isPresent()) return maybeRegistry; + } + } + } + + return (Optional<? extends Registry<T>>) Registry.REGISTRIES.getOrEmpty(tagKey.registry().getValue()); + } +} diff --git a/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/impl/tag/client/ClientTagsLoader.java b/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/impl/tag/client/ClientTagsLoader.java new file mode 100644 index 000000000..ecb70d623 --- /dev/null +++ b/fabric-client-tags-api-v1/src/client/java/net/fabricmc/fabric/impl/tag/client/ClientTagsLoader.java @@ -0,0 +1,129 @@ +/* + * 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.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.JsonOps; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.tag.TagEntry; +import net.minecraft.tag.TagFile; +import net.minecraft.tag.TagKey; +import net.minecraft.tag.TagManagerLoader; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; +import net.minecraft.util.registry.RegistryKey; + +import net.fabricmc.fabric.api.tag.client.v1.ClientTags; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; + +@ApiStatus.Internal +public class ClientTagsLoader { + private static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-tags-api-v1"); + /** + * Load a given tag from the available mods into a set of {@code Identifier}s. + * Parsing based on {@link net.minecraft.tag.TagGroupLoader#loadTags(net.minecraft.resource.ResourceManager)} + */ + public static Set<Identifier> loadTag(TagKey<?> tagKey) { + var tags = new HashSet<TagEntry>(); + HashSet<Path> tagFiles = getTagFiles(tagKey.registry(), tagKey.id()); + + for (Path tagPath : tagFiles) { + try (BufferedReader tagReader = Files.newBufferedReader(tagPath)) { + JsonElement jsonElement = JsonParser.parseReader(tagReader); + TagFile maybeTagFile = TagFile.CODEC.parse(new Dynamic<>(JsonOps.INSTANCE, jsonElement)) + .result().orElse(null); + + if (maybeTagFile != null) { + if (maybeTagFile.replace()) { + tags.clear(); + } + + tags.addAll(maybeTagFile.entries()); + } + } catch (IOException e) { + LOGGER.error("Error loading tag: " + tagKey, e); + } + } + + HashSet<Identifier> ids = new HashSet<>(); + + for (TagEntry tagEntry : tags) { + tagEntry.resolve(new TagEntry.ValueGetter<>() { + @Nullable + @Override + public Identifier direct(Identifier id) { + return id; + } + + @Nullable + @Override + public Collection<Identifier> tag(Identifier id) { + TagKey<?> tag = TagKey.of(tagKey.registry(), id); + return ClientTags.getOrCreateLocalTag(tag); + } + }, ids::add); + } + + return Collections.unmodifiableSet(ids); + } + + /** + * @param registryKey the RegistryKey of the TagKey + * @param identifier the Identifier of the tag + * @return the paths to all tag json files within the available mods + */ + private static HashSet<Path> getTagFiles(RegistryKey<? extends Registry<?>> registryKey, Identifier identifier) { + return getTagFiles(TagManagerLoader.getPath(registryKey), identifier); + } + + /** + * @return the paths to all tag json files within the available mods + */ + private static HashSet<Path> getTagFiles(String tagType, Identifier identifier) { + String tagFile = "data/%s/%s/%s.json".formatted(identifier.getNamespace(), tagType, identifier.getPath()); + return getResourcePaths(tagFile); + } + + /** + * @return all paths from the available mods that match the given internal path + */ + private static HashSet<Path> getResourcePaths(String path) { + HashSet<Path> out = new HashSet<>(); + + for (ModContainer mod : FabricLoader.getInstance().getAllMods()) { + mod.findPath(path).ifPresent(out::add); + } + + return out; + } +} diff --git a/fabric-client-tags-api-v1/src/client/resources/assets/fabric-client-tags-api-v1/icon.png b/fabric-client-tags-api-v1/src/client/resources/assets/fabric-client-tags-api-v1/icon.png new file mode 100644 index 000000000..2931efbf6 Binary files /dev/null and b/fabric-client-tags-api-v1/src/client/resources/assets/fabric-client-tags-api-v1/icon.png differ diff --git a/fabric-client-tags-api-v1/src/client/resources/fabric.mod.json b/fabric-client-tags-api-v1/src/client/resources/fabric.mod.json new file mode 100644 index 000000000..504cd5b81 --- /dev/null +++ b/fabric-client-tags-api-v1/src/client/resources/fabric.mod.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "fabric-client-tags-api-v1", + "name": "Fabric Client Tags", + "version": "${version}", + "environment": "client", + "license": "Apache-2.0", + "icon": "assets/fabric-client-tags-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.14.6" + }, + "description": "Adds the ability to load tags from the local mods.", + "custom": { + "fabric-api:module-lifecycle": "stable" + } +} diff --git a/fabric-client-tags-api-v1/src/testmod/java/net/fabricmc/fabric/test/tag/client/v1/ClientTagTest.java b/fabric-client-tags-api-v1/src/testmod/java/net/fabricmc/fabric/test/tag/client/v1/ClientTagTest.java new file mode 100644 index 000000000..eb2d95c5f --- /dev/null +++ b/fabric-client-tags-api-v1/src/testmod/java/net/fabricmc/fabric/test/tag/client/v1/ClientTagTest.java @@ -0,0 +1,58 @@ +/* + * 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.client.v1; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.block.Blocks; +import net.minecraft.world.biome.BiomeKeys; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.tag.client.v1.ClientTags; +import net.fabricmc.fabric.api.tag.convention.v1.ConventionalBiomeTags; +import net.fabricmc.fabric.api.tag.convention.v1.ConventionalBlockTags; +import net.fabricmc.fabric.api.tag.convention.v1.ConventionalEnchantmentTags; + +public class ClientTagTest implements ClientModInitializer { + private static final Logger LOGGER = LoggerFactory.getLogger(ClientTagTest.class); + + @Override + public void onInitializeClient() { + ClientLifecycleEvents.CLIENT_STARTED.register(client -> { + if (ClientTags.getOrCreateLocalTag(ConventionalEnchantmentTags.INCREASES_BLOCK_DROPS) == null) { + throw new AssertionError("Expected to load c:fortune, but it was not found!"); + } + + if (!ClientTags.isInWithLocalFallback(ConventionalBlockTags.ORES, Blocks.DIAMOND_ORE)) { + throw new AssertionError("Expected to find diamond ore in c:ores, but it was not found!"); + } + + if (ClientTags.isInWithLocalFallback(ConventionalBlockTags.ORES, Blocks.DIAMOND_BLOCK)) { + throw new AssertionError("Did not expect to find diamond block in c:ores, but it was found!"); + } + + if (!ClientTags.isInLocal(ConventionalBiomeTags.FOREST, BiomeKeys.FOREST)) { + throw new AssertionError("Expected to find forest in c:forest, but it was not found!"); + } + + // Success! + LOGGER.info("The tests for client tags passed!"); + }); + } +} diff --git a/fabric-client-tags-api-v1/src/testmod/resources/fabric.mod.json b/fabric-client-tags-api-v1/src/testmod/resources/fabric.mod.json new file mode 100644 index 000000000..5f4d62b0a --- /dev/null +++ b/fabric-client-tags-api-v1/src/testmod/resources/fabric.mod.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "fabric-clients-tags-api-v1-testmod", + "name": "Fabric Client Tags API (v1) Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-convention-tags-v1": "*" + }, + "entrypoints": { + "client": [ + "net.fabricmc.fabric.test.tag.client.v1.ClientTagTest" + ] + } +} diff --git a/gradle.properties b/gradle.properties index f72d11f02..09da69d3c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -56,3 +56,4 @@ fabric-textures-v0-version=1.0.18 fabric-transfer-api-v1-version=2.0.9 fabric-transitive-access-wideners-v1-version=1.1.1 fabric-convention-tags-v1-version=1.0.8 +fabric-client-tags-api-v1-version=1.0.0 diff --git a/settings.gradle b/settings.gradle index ac0d45fab..2cb8fd438 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,6 +50,7 @@ include 'fabric-screen-handler-api-v1' include 'fabric-textures-v0' include 'fabric-transfer-api-v1' include 'fabric-convention-tags-v1' +include 'fabric-client-tags-api-v1' include 'fabric-transitive-access-wideners-v1' include 'deprecated'