Add support for partially synced client tags (#3118)

* Draft v1.1.0

* Resolve some comments

* Add javadoc

* Remove old behavior

* Minor cleanup

* Add test for partially synced tags

* Address nitpick

* Fix checkstyle

* Hard fail when datapack fails to regsiter

Co-authored-by: modmuss <modmuss50@gmail.com>

* Fix missing import

* Refactor
Don't recurse through tag hierarchy

* Add note for test

* Adjustments to logic to handle server-missing nested tags

* Restore recursive search, add tracking of checked tags

* Cleanup

---------

Co-authored-by: modmuss <modmuss50@gmail.com>
This commit is contained in:
Deximus-Maximus 2023-07-03 08:10:52 -04:00 committed by GitHub
parent 6beca848ff
commit 97bb207586
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 225 additions and 86 deletions

View file

@ -6,4 +6,5 @@ moduleDependencies(project, ['fabric-api-base'])
testDependencies(project, [
':fabric-convention-tags-v1',
':fabric-lifecycle-events-v1',
':fabric-resource-loader-v0',
])

View file

@ -16,21 +16,15 @@
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.registry.RegistryKey;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.util.Identifier;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.registry.RegistryKey;
import net.fabricmc.fabric.impl.tag.client.ClientTagsLoader;
import net.fabricmc.fabric.impl.tag.client.ClientTagsImpl;
/**
* Allows the use of tags by directly loading them from the installed mods.
@ -45,8 +39,6 @@ import net.fabricmc.fabric.impl.tag.client.ClientTagsLoader;
* even when connected to a vanilla server.
*/
public final class ClientTags {
private static final Map<TagKey<?>, Set<Identifier>> LOCAL_TAG_CACHE = new ConcurrentHashMap<>();
private ClientTags() {
}
@ -57,54 +49,25 @@ public final class ClientTags {
* @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;
return ClientTagsImpl.getOrCreatePartiallySyncedTag(tagKey).completeIds();
}
/**
* 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.
* the tag populated from the available mods is checked, recursively checking the
* synced tags and entries contained within.
*
* @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.getEntryList(tagKey).isPresent()) {
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();
return ClientTagsImpl.getRegistryEntry(tagKey, entry).map(re -> isInWithLocalFallback(tagKey, re)).orElse(false);
}
/**
@ -112,7 +75,8 @@ public final class ClientTags {
* 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.
* the tag populated from the available mods is checked, recursively checking the
* synced tags and entries contained within.
*
* @param tagKey the {@code TagKey} to be checked
* @param registryEntry the entry to check
@ -121,21 +85,7 @@ public final class ClientTags {
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().getEntryList(tagKey).isPresent()) {
return registryEntry.isIn(tagKey);
}
}
if (registryEntry.getKey().isPresent()) {
return isInLocal(tagKey, registryEntry.getKey().get());
}
return false;
return ClientTagsImpl.isInWithLocalFallback(tagKey, registryEntry);
}
/**
@ -157,22 +107,4 @@ public final class ClientTags {
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>>) Registries.REGISTRIES.getOrEmpty(tagKey.registry().getValue());
}
}

View file

@ -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.impl.tag.client;
import java.util.HashSet;
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.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.registry.tag.TagKey;
public class ClientTagsImpl {
private static final Map<TagKey<?>, ClientTagsLoader.LoadedTag> LOCAL_TAG_HIERARCHY = new ConcurrentHashMap<>();
public static <T> boolean isInWithLocalFallback(TagKey<T> tagKey, RegistryEntry<T> registryEntry) {
return isInWithLocalFallback(tagKey, registryEntry, new HashSet<>());
}
@SuppressWarnings("unchecked")
private static <T> boolean isInWithLocalFallback(TagKey<T> tagKey, RegistryEntry<T> registryEntry, Set<TagKey<T>> checked) {
if (checked.contains(tagKey)) {
return false;
}
checked.add(tagKey);
// Check if the tag exists in the dynamic registry first
Optional<? extends Registry<T>> maybeRegistry = ClientTagsImpl.getRegistry(tagKey);
if (maybeRegistry.isPresent()) {
// Check the synced tag exists and use that
if (maybeRegistry.get().getEntryList(tagKey).isPresent()) {
return registryEntry.isIn(tagKey);
}
}
if (registryEntry.getKey().isEmpty()) {
// No key?
return false;
}
// Recursively search the entries contained with the tag
ClientTagsLoader.LoadedTag wt = ClientTagsImpl.getOrCreatePartiallySyncedTag(tagKey);
if (wt.immediateChildIds().contains(registryEntry.getKey().get().getValue())) {
return true;
}
for (TagKey<?> key : wt.immediateChildTags()) {
if (isInWithLocalFallback((TagKey<T>) key, registryEntry, checked)) {
return true;
}
checked.add((TagKey<T>) key);
}
return false;
}
@SuppressWarnings("unchecked")
public 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>>) Registries.REGISTRIES.getOrEmpty(tagKey.registry().getValue());
}
@SuppressWarnings("unchecked")
public static <T> Optional<RegistryEntry<T>> getRegistryEntry(TagKey<T> tagKey, T entry) {
Optional<? extends Registry<?>> maybeRegistry = getRegistry(tagKey);
if (maybeRegistry.isEmpty() || !tagKey.isOf(maybeRegistry.get().getKey())) {
return Optional.empty();
}
Registry<T> registry = (Registry<T>) maybeRegistry.get();
Optional<RegistryKey<T>> maybeKey = registry.getKey(entry);
return maybeKey.map(registry::entryOf);
}
public static ClientTagsLoader.LoadedTag getOrCreatePartiallySyncedTag(TagKey<?> tagKey) {
ClientTagsLoader.LoadedTag loadedTag = LOCAL_TAG_HIERARCHY.get(tagKey);
if (loadedTag == null) {
loadedTag = ClientTagsLoader.loadTag(tagKey);
LOCAL_TAG_HIERARCHY.put(tagKey, loadedTag);
}
return loadedTag;
}
}

View file

@ -33,15 +33,14 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.tag.TagEntry;
import net.minecraft.registry.tag.TagFile;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.registry.tag.TagManagerLoader;
import net.minecraft.util.Identifier;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.fabricmc.fabric.api.tag.client.v1.ClientTags;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
@ -51,7 +50,7 @@ public class ClientTagsLoader {
* Load a given tag from the available mods into a set of {@code Identifier}s.
* Parsing based on {@link net.minecraft.registry.tag.TagGroupLoader#loadTags(net.minecraft.resource.ResourceManager)}
*/
public static Set<Identifier> loadTag(TagKey<?> tagKey) {
public static LoadedTag loadTag(TagKey<?> tagKey) {
var tags = new HashSet<TagEntry>();
HashSet<Path> tagFiles = getTagFiles(tagKey.registry(), tagKey.id());
@ -73,13 +72,16 @@ public class ClientTagsLoader {
}
}
HashSet<Identifier> ids = new HashSet<>();
HashSet<Identifier> completeIds = new HashSet<>();
HashSet<Identifier> immediateChildIds = new HashSet<>();
HashSet<TagKey<?>> immediateChildTags = new HashSet<>();
for (TagEntry tagEntry : tags) {
tagEntry.resolve(new TagEntry.ValueGetter<>() {
@Nullable
@Override
public Identifier direct(Identifier id) {
immediateChildIds.add(id);
return id;
}
@ -87,12 +89,20 @@ public class ClientTagsLoader {
@Override
public Collection<Identifier> tag(Identifier id) {
TagKey<?> tag = TagKey.of(tagKey.registry(), id);
return ClientTags.getOrCreateLocalTag(tag);
immediateChildTags.add(tag);
return ClientTagsImpl.getOrCreatePartiallySyncedTag(tag).completeIds;
}
}, ids::add);
}, completeIds::add);
}
return Collections.unmodifiableSet(ids);
// Ensure that the tag does not refer to itself
immediateChildTags.remove(tagKey);
return new LoadedTag(Collections.unmodifiableSet(completeIds), Collections.unmodifiableSet(immediateChildTags),
Collections.unmodifiableSet(immediateChildIds));
}
public record LoadedTag(Set<Identifier> completeIds, Set<TagKey<?>> immediateChildTags, Set<Identifier> immediateChildIds) {
}
/**

View file

@ -20,20 +20,36 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.minecraft.block.Blocks;
import net.minecraft.registry.Registries;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.util.Identifier;
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.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
import net.fabricmc.fabric.api.resource.ResourcePackActivationType;
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;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
public class ClientTagTest implements ClientModInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger(ClientTagTest.class);
private static final String MODID = "fabric-clients-tags-api-v1-testmod";
@Override
public void onInitializeClient() {
final ModContainer container = FabricLoader.getInstance().getModContainer(MODID).get();
if (!ResourceManagerHelper.registerBuiltinResourcePack(new Identifier(MODID, "test2"),
container, ResourcePackActivationType.ALWAYS_ENABLED)) {
throw new IllegalStateException("Could not register built-in resource pack.");
}
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!");
@ -51,8 +67,23 @@ public class ClientTagTest implements ClientModInitializer {
throw new AssertionError("Expected to find forest in c:forest, but it was not found!");
}
if (ClientTags.isInWithLocalFallback(TagKey.of(Registries.BLOCK.getKey(),
new Identifier("fabric", "sword_efficient")), Blocks.DIRT)) {
throw new AssertionError("Expected not to find dirt in fabric:sword_efficient, but it was found!");
}
// Success!
LOGGER.info("The tests for client tags passed!");
});
// This should be tested on a server with the datapack from the builtin resourcepack.
// That is, fabric:sword_efficient should NOT exist on the server (can be confirmed with F3 on a dirt block),
// but the this test should pass as minecraft:sword_efficient will contain dirt on the server
ClientTickEvents.END_WORLD_TICK.register(client -> {
if (!ClientTags.isInWithLocalFallback(TagKey.of(Registries.BLOCK.getKey(),
new Identifier("fabric", "sword_efficient")), Blocks.DIRT)) {
throw new AssertionError("Expected to find dirt in fabric:sword_efficient, but it was not found!");
}
});
}
}

View file

@ -0,0 +1,25 @@
{
"replace": false,
"values": [
{
"id": "#fabric:mineable/sword",
"required": false
},
{
"id": "#minecraft:sword_efficient",
"required": false
},
{
"id": "minecraft:bamboo",
"required": false
},
{
"id": "minecraft:cobweb",
"required": false
},
{
"id": "minecraft:bamboo_sapling",
"required": false
}
]
}

View file

@ -0,0 +1,10 @@
{
"replace": false,
"values": [
"minecraft:dirt",
{
"id": "",
"required": false
}
]
}

View file

@ -0,0 +1,6 @@
{
"pack": {
"pack_format": 9,
"description": "Test Dirt in SwordEfficient"
}
}