mirror of
https://github.com/FabricMC/fabric.git
synced 2025-05-23 11:36:47 -04:00
Add client tags module (#2308)
* 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:
parent
6718a028fc
commit
b35fea83d3
9 changed files with 421 additions and 0 deletions
fabric-client-tags-api-v1
gradle.propertiessettings.gradle
11
fabric-client-tags-api-v1/build.gradle
Normal file
11
fabric-client-tags-api-v1/build.gradle
Normal 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',
|
||||
])
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue