Add client tags module ()

* Add client tags module

* Fix not actually returning the tag eval. for dynamic registry fallback

* Use dynamic registry where needed

* Make ClientTags uninititializable

* Resolve feedback

* Change package

Co-authored-by: deirn <deirn@bai.lol>

* Change package

Co-authored-by: deirn <deirn@bai.lol>

* Remove unused dep. block

Co-authored-by: deirn <deirn@bai.lol>

* Fix imports

* Move the files to match new package

* Apply feedback

* Apply feedback

* Add test mod

* Fix CME

* Fix checkstyle

* Apply feedback

* Apply feedback

* Apply feedback

* Fix checkstyle

Co-authored-by: deirn <deirn@bai.lol>
This commit is contained in:
Deximus-Maximus 2022-08-06 14:02:27 -04:00 committed by GitHub
parent 6718a028fc
commit b35fea83d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 421 additions and 0 deletions
fabric-client-tags-api-v1
build.gradle
src
client
java/net/fabricmc/fabric
api/tag/client/v1
impl/tag/client
resources
assets/fabric-client-tags-api-v1
fabric.mod.json
testmod
java/net/fabricmc/fabric/test/tag/client/v1
resources
gradle.propertiessettings.gradle

View file

@ -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',
])

View file

@ -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());
}
}

View file

@ -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;
}
}

Binary file not shown.

After

(image error) Size: 1.5 KiB

View file

@ -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"
}
}

View file

@ -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!");
});
}
}

View file

@ -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"
]
}
}

View file

@ -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

View file

@ -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'