From e63306e01528ffbd2d1e8217cbcf97285fe346fb Mon Sep 17 00:00:00 2001
From: Technici4n <13494793+Technici4n@users.noreply.github.com>
Date: Mon, 13 Feb 2023 10:30:18 +0100
Subject: [PATCH] Resource Conditions Additions (#2821)

* Resource Conditions Additions

- Add `registry_contains` condition. Closes #2548.
- Make `fabric:load_conditions` appear first in generated JSON objects.
- Uniformize implementation a bit.

* Update fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataProviderMixin.java

Co-authored-by: Juuz <6596629+Juuxel@users.noreply.github.com>

* itemsLoaded -> itemsRegistered

---------

Co-authored-by: Juuz <6596629+Juuxel@users.noreply.github.com>
---
 .../datagen/DataGeneratorTestEntrypoint.java  |   8 ++
 .../v1/DefaultResourceConditions.java         |  70 ++++++++++
 .../conditions/ResourceConditionsImpl.java    | 120 ++++++++++++++----
 .../conditions/DataPackContentsMixin.java     |   6 +-
 .../conditions/DataProviderMixin.java         |  40 ++++++
 ...ric-resource-conditions-api-v1.mixins.json |   1 +
 6 files changed, 215 insertions(+), 30 deletions(-)
 create mode 100644 fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataProviderMixin.java

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 ee906617b..1d0710713 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
@@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
 import net.minecraft.advancement.Advancement;
 import net.minecraft.advancement.AdvancementFrame;
 import net.minecraft.advancement.criterion.OnKilledCriterion;
+import net.minecraft.block.Blocks;
 import net.minecraft.registry.RegistryKeys;
 import net.minecraft.data.client.BlockStateModelGenerator;
 import net.minecraft.data.client.ItemModelGenerator;
@@ -108,6 +109,13 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
 		public void generate(Consumer<RecipeJsonProvider> exporter) {
 			offerPlanksRecipe2(exporter, SIMPLE_BLOCK, ItemTags.ACACIA_LOGS, 1);
 
+			ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.LEATHER, 4).input(Items.ITEM_FRAME)
+					.criterion("has_frame", conditionsFromItem(Items.ITEM_FRAME))
+					.offerTo(withConditions(exporter, DefaultResourceConditions.itemsRegistered(Blocks.DIAMOND_BLOCK)));
+			ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.LEATHER_BOOTS, 4).input(Items.ITEM_FRAME, 2)
+					.criterion("has_frame", conditionsFromItem(Items.ITEM_FRAME))
+					.offerTo(withConditions(exporter, DefaultResourceConditions.registryContains(BiomeKeys.PLAINS, BiomeKeys.BADLANDS)));
+
 			ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.GOLD_INGOT).input(Items.DIRT).criterion("has_dirt", conditionsFromItem(Items.DIRT)).offerTo(withConditions(exporter, NEVER_LOADED));
 			ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.DIAMOND).input(Items.STICK).criterion("has_stick", conditionsFromItem(Items.STICK)).offerTo(withConditions(exporter, ALWAYS_LOADED));
 
diff --git a/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/api/resource/conditions/v1/DefaultResourceConditions.java b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/api/resource/conditions/v1/DefaultResourceConditions.java
index 8e5c029df..1a8886cfb 100644
--- a/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/api/resource/conditions/v1/DefaultResourceConditions.java
+++ b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/api/resource/conditions/v1/DefaultResourceConditions.java
@@ -16,12 +16,20 @@
 
 package net.fabricmc.fabric.api.resource.conditions.v1;
 
+import java.util.Arrays;
+import java.util.function.Function;
+
+import com.google.common.base.Preconditions;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 
 import net.minecraft.block.Block;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.item.Item;
+import net.minecraft.item.ItemConvertible;
+import net.minecraft.registry.Registries;
+import net.minecraft.registry.Registry;
+import net.minecraft.registry.RegistryKey;
 import net.minecraft.registry.RegistryKeys;
 import net.minecraft.registry.tag.TagKey;
 import net.minecraft.resource.featuretoggle.FeatureFlag;
@@ -44,6 +52,7 @@ public final class DefaultResourceConditions {
 	private static final Identifier ITEM_TAGS_POPULATED = new Identifier("fabric:item_tags_populated");
 	private static final Identifier TAGS_POPULATED = new Identifier("fabric:tags_populated");
 	private static final Identifier FEATURES_ENABLED = new Identifier("fabric:features_enabled");
+	private static final Identifier REGISTRY_CONTAINS = new Identifier("fabric:registry_contains");
 
 	/**
 	 * Creates a NOT condition that returns true if its child condition is false, and false if its child is true.
@@ -159,6 +168,54 @@ public final class DefaultResourceConditions {
 		return ResourceConditionsImpl.featuresEnabled(FEATURES_ENABLED, features);
 	}
 
+	/**
+	 * Creates a condition that returns true if all the passed items are registered (in {@link Registries#ITEM}).
+	 *
+	 * @see #registryContains(RegistryKey, Identifier...)
+	 */
+	public static ConditionJsonProvider itemsRegistered(ItemConvertible... items) {
+		return registryContains(Registries.ITEM, transform(items, ItemConvertible::asItem));
+	}
+
+	/**
+	 * Creates a condition that returns true if the registry contains all the passed entries,
+	 * i.e. if all the passed registry entries are loaded.
+	 *
+	 * @see #registryContains(RegistryKey, Identifier...)
+	 */
+	@SafeVarargs
+	public static <T> ConditionJsonProvider registryContains(Registry<T> registry, T... entries) {
+		return registryContains(transform(entries, e -> {
+			return registry.getKey(e).orElseThrow(() -> new IllegalArgumentException("Entry is not registered"));
+		}));
+	}
+
+	/**
+	 * Creates a condition that returns true if all the passed registry entries are loaded.
+	 *
+	 * @see #registryContains(RegistryKey, Identifier...)
+	 */
+	@SafeVarargs
+	public static <T> ConditionJsonProvider registryContains(RegistryKey<T>... entries) {
+		Preconditions.checkArgument(entries.length > 0, "Must register at least one entry.");
+
+		return registryContains(
+				RegistryKey.ofRegistry(entries[0].getRegistry()),
+				transform(entries, RegistryKey::getValue));
+	}
+
+	/**
+	 * Creates a condition that returns true if all the passed registry entries are loaded.
+	 * Dynamic registries are supported for server resources.
+	 *
+	 * @apiNote This condition's ID is {@code fabric:registry_contains}, and takes up to two properties:
+	 * {@code values}, which is an array of string registry entry IDs, and {@code registry}, which is the ID of
+	 * the registry of the entries. If {@code registry} is not provided, it defaults to {@code minecraft:item}.
+	 */
+	public static <T> ConditionJsonProvider registryContains(RegistryKey<Registry<T>> registry, Identifier... entries) {
+		return ResourceConditionsImpl.registryContains(REGISTRY_CONTAINS, registry.getValue(), entries);
+	}
+
 	static void init() {
 		// init static
 	}
@@ -183,6 +240,19 @@ public final class DefaultResourceConditions {
 		ResourceConditions.register(ITEM_TAGS_POPULATED, object -> ResourceConditionsImpl.tagsPopulatedMatch(object, RegistryKeys.ITEM));
 		ResourceConditions.register(TAGS_POPULATED, ResourceConditionsImpl::tagsPopulatedMatch);
 		ResourceConditions.register(FEATURES_ENABLED, ResourceConditionsImpl::featuresEnabledMatch);
+		ResourceConditions.register(REGISTRY_CONTAINS, ResourceConditionsImpl::registryContainsMatch);
+	}
+
+	// Slightly gross - the empty outputType vararg is used to capture the correct type for B[]
+	@SafeVarargs
+	private static <A, B> B[] transform(A[] input, Function<A, B> mapper, B... outputType) {
+		B[] output = Arrays.copyOf(outputType, input.length);
+
+		for (int i = 0; i < input.length; i++) {
+			output[i] = mapper.apply(input[i]);
+		}
+
+		return output;
 	}
 
 	private DefaultResourceConditions() {
diff --git a/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/impl/resource/conditions/ResourceConditionsImpl.java b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/impl/resource/conditions/ResourceConditionsImpl.java
index 0e345c289..315725e08 100644
--- a/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/impl/resource/conditions/ResourceConditionsImpl.java
+++ b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/impl/resource/conditions/ResourceConditionsImpl.java
@@ -20,6 +20,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -32,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import net.minecraft.registry.DynamicRegistryManager;
 import net.minecraft.registry.Registry;
 import net.minecraft.registry.RegistryKey;
 import net.minecraft.registry.RegistryKeys;
@@ -126,6 +128,55 @@ public final class ResourceConditionsImpl {
 		};
 	}
 
+	public static ConditionJsonProvider featuresEnabled(Identifier id, final FeatureFlag... features) {
+		final Set<Identifier> ids = new TreeSet<>(FeatureFlags.FEATURE_MANAGER.toId(FeatureFlags.FEATURE_MANAGER.featureSetOf(features)));
+
+		return new ConditionJsonProvider() {
+			@Override
+			public Identifier getConditionId() {
+				return id;
+			}
+
+			@Override
+			public void writeParameters(JsonObject object) {
+				JsonArray array = new JsonArray();
+
+				for (Identifier id : ids) {
+					array.add(id.toString());
+				}
+
+				object.add("features", array);
+			}
+		};
+	}
+
+	public static ConditionJsonProvider registryContains(Identifier id, Identifier registry, Identifier... entries) {
+		Preconditions.checkArgument(entries.length > 0, "Must register at least one entry.");
+
+		return new ConditionJsonProvider() {
+			@Override
+			public Identifier getConditionId() {
+				return id;
+			}
+
+			@Override
+			public void writeParameters(JsonObject object) {
+				JsonArray array = new JsonArray();
+
+				for (Identifier entry : entries) {
+					array.add(entry.toString());
+				}
+
+				object.add("values", array);
+
+				if (!RegistryKeys.ITEM.getValue().equals(registry)) {
+					// Skip if this is the default (minecraft:item)
+					object.addProperty("registry", registry.toString());
+				}
+			}
+		};
+	}
+
 	// Condition implementations
 
 	public static boolean modsLoadedMatch(JsonObject object, boolean and) {
@@ -165,10 +216,6 @@ public final class ResourceConditionsImpl {
 		LOADED_TAGS.set(tagMap);
 	}
 
-	public static void clearTags() {
-		LOADED_TAGS.remove();
-	}
-
 	public static boolean tagsPopulatedMatch(JsonObject object) {
 		String key = JsonHelper.getString(object, "registry", "minecraft:item");
 		RegistryKey<? extends Registry<?>> registryRef = RegistryKey.ofRegistry(new Identifier(key));
@@ -208,29 +255,7 @@ public final class ResourceConditionsImpl {
 		return true;
 	}
 
-	public static ConditionJsonProvider featuresEnabled(Identifier id, final FeatureFlag... features) {
-		final Set<Identifier> ids = new TreeSet<>(FeatureFlags.FEATURE_MANAGER.toId(FeatureFlags.FEATURE_MANAGER.featureSetOf(features)));
-
-		return new ConditionJsonProvider() {
-			@Override
-			public Identifier getConditionId() {
-				return id;
-			}
-
-			@Override
-			public void writeParameters(JsonObject object) {
-				JsonArray array = new JsonArray();
-
-				for (Identifier id : ids) {
-					array.add(id.toString());
-				}
-
-				object.add("features", array);
-			}
-		};
-	}
-
-	public static ThreadLocal<FeatureSet> currentFeature = ThreadLocal.withInitial(() -> FeatureFlags.DEFAULT_ENABLED_FEATURES);
+	public static final ThreadLocal<FeatureSet> CURRENT_FEATURES = ThreadLocal.withInitial(() -> FeatureFlags.DEFAULT_ENABLED_FEATURES);
 
 	public static boolean featuresEnabledMatch(JsonObject object) {
 		List<Identifier> featureIds = JsonHelper.getArray(object, "features").asList().stream().map((element) -> new Identifier(element.getAsString())).toList();
@@ -238,6 +263,45 @@ public final class ResourceConditionsImpl {
 			throw new JsonParseException("Unknown feature flag: " + id);
 		});
 
-		return set.isSubsetOf(currentFeature.get());
+		return set.isSubsetOf(CURRENT_FEATURES.get());
+	}
+
+	public static final ThreadLocal<DynamicRegistryManager.Immutable> CURRENT_REGISTRIES = new ThreadLocal<>();
+
+	public static boolean registryContainsMatch(JsonObject object) {
+		String key = JsonHelper.getString(object, "registry", "minecraft:item");
+		RegistryKey<? extends Registry<?>> registryRef = RegistryKey.ofRegistry(new Identifier(key));
+		return registryContainsMatch(object, registryRef);
+	}
+
+	private static <E> boolean registryContainsMatch(JsonObject object, RegistryKey<? extends Registry<? extends E>> registryRef) {
+		JsonArray array = JsonHelper.getArray(object, "values");
+		DynamicRegistryManager.Immutable registries = CURRENT_REGISTRIES.get();
+
+		if (registries == null) {
+			LOGGER.warn("Can't retrieve current registries. Failing registry_contains resource condition check.");
+			return false;
+		}
+
+		Optional<Registry<E>> registry = registries.getOptional(registryRef);
+
+		if (registry.isEmpty()) {
+			// No such registry
+			return array.isEmpty();
+		}
+
+		for (JsonElement element : array) {
+			if (element.isJsonPrimitive()) {
+				Identifier id = new Identifier(element.getAsString());
+
+				if (!registry.get().containsId(id)) {
+					return false;
+				}
+			} else {
+				throw new JsonParseException("Invalid registry entry id: " + element);
+			}
+		}
+
+		return true;
 	}
 }
diff --git a/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataPackContentsMixin.java b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataPackContentsMixin.java
index 1eb18b40a..dd8314341 100644
--- a/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataPackContentsMixin.java
+++ b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataPackContentsMixin.java
@@ -44,7 +44,8 @@ public class DataPackContentsMixin {
 			at = @At("HEAD")
 	)
 	public void hookRefresh(DynamicRegistryManager dynamicRegistryManager, CallbackInfo ci) {
-		ResourceConditionsImpl.clearTags();
+		ResourceConditionsImpl.LOADED_TAGS.remove();
+		ResourceConditionsImpl.CURRENT_REGISTRIES.remove();
 	}
 
 	@Inject(
@@ -52,6 +53,7 @@ public class DataPackContentsMixin {
 			at = @At("HEAD")
 	)
 	private static void hookReload(ResourceManager manager, DynamicRegistryManager.Immutable dynamicRegistryManager, FeatureSet enabledFeatures, CommandManager.RegistrationEnvironment environment, int functionPermissionLevel, Executor prepareExecutor, Executor applyExecutor, CallbackInfoReturnable<CompletableFuture<DataPackContents>> cir) {
-		ResourceConditionsImpl.currentFeature.set(enabledFeatures);
+		ResourceConditionsImpl.CURRENT_FEATURES.set(enabledFeatures);
+		ResourceConditionsImpl.CURRENT_REGISTRIES.set(dynamicRegistryManager);
 	}
 }
diff --git a/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataProviderMixin.java b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataProviderMixin.java
new file mode 100644
index 000000000..c3c42292a
--- /dev/null
+++ b/fabric-resource-conditions-api-v1/src/main/java/net/fabricmc/fabric/mixin/resource/conditions/DataProviderMixin.java
@@ -0,0 +1,40 @@
+/*
+ * 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.resource.conditions;
+
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import org.spongepowered.asm.mixin.Dynamic;
+import org.spongepowered.asm.mixin.Mixin;
+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.data.DataProvider;
+
+import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
+
+/**
+ * Make the {@value ResourceConditions#CONDITIONS_KEY} appear first in generated JSON objects.
+ */
+@Mixin(DataProvider.class)
+public interface DataProviderMixin {
+	@Dynamic("lambda method passed to Util.make")
+	@Inject(method = "method_43808", at = @At("HEAD"))
+	private static void fabric_injectResourceConditionsSortOrder(Object2IntOpenHashMap<String> map, CallbackInfo ci) {
+		map.put(ResourceConditions.CONDITIONS_KEY, -100);
+	}
+}
diff --git a/fabric-resource-conditions-api-v1/src/main/resources/fabric-resource-conditions-api-v1.mixins.json b/fabric-resource-conditions-api-v1/src/main/resources/fabric-resource-conditions-api-v1.mixins.json
index 1d03936d4..a77c8f7e2 100644
--- a/fabric-resource-conditions-api-v1/src/main/resources/fabric-resource-conditions-api-v1.mixins.json
+++ b/fabric-resource-conditions-api-v1/src/main/resources/fabric-resource-conditions-api-v1.mixins.json
@@ -4,6 +4,7 @@
   "compatibilityLevel": "JAVA_16",
   "mixins": [
     "DataPackContentsMixin",
+    "DataProviderMixin",
     "JsonDataLoaderMixin",
     "SinglePreparationResourceReloaderMixin",
     "TagManagerLoaderMixin"