[1.20.5] Codec-based Resource Conditions and refactors ()

* Initial move to codec-based resource conditions

* Move default condition types to DefaultResourceConditionTypes

* Move built-in condition types to impl

* Run spotlessApply

* Move default condition registration to entrypoint in imp

* Formatting fixes

* Add back package-info.java

* Make it compatible with new DFU

* checkstyle

* Refactor FeaturesEnabledResourceCondition

* Pass RegistryWrapper and reduce reliance on statics

* Some more improvements

* Add back some tests

* Add more tests

* Add back tags_populated support, clean up code

* Some more improvements

* add javadocs (wip)

* Fix unfixed merge conflict

* Javadocs

* Fix null condition in addConditions

* Allow empty arrays in certain codecs

* Move addConditions to datagen impl

* Address reviews

* add debug log

* Store features in normal static field

* Fix TagsPopulatedResourceCondition#getType

---------

Co-authored-by: Apollo <102649729+Apollounknowndev@users.noreply.github.com>
This commit is contained in:
apple502j 2024-04-10 23:40:15 +09:00 committed by GitHub
parent 9d8968f5a3
commit 3b6dc5deb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1330 additions and 757 deletions
fabric-data-generation-api-v1/src
fabric-resource-conditions-api-v1

View file

@ -20,7 +20,7 @@ import com.google.common.base.Preconditions;
import net.minecraft.data.server.loottable.BlockLootTableGenerator;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.impl.datagen.loot.ConditionBlockLootTableGenerator;
/**
@ -33,7 +33,7 @@ public interface FabricBlockLootTableGenerator {
* Return a new generator that applies the specified conditions to any loot table it receives,
* and then forwards the loot tables to this generator.
*/
default BlockLootTableGenerator withConditions(ConditionJsonProvider... conditions) {
default BlockLootTableGenerator withConditions(ResourceCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must add at least one condition.");
return new ConditionBlockLootTableGenerator((BlockLootTableGenerator) this, conditions);

View file

@ -40,7 +40,7 @@ import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
/**
@ -69,7 +69,7 @@ public abstract class FabricAdvancementProvider implements DataProvider {
/**
* Return a new exporter that applies the specified conditions to any advancement it receives.
*/
protected Consumer<AdvancementEntry> withConditions(Consumer<AdvancementEntry> exporter, ConditionJsonProvider... conditions) {
protected Consumer<AdvancementEntry> withConditions(Consumer<AdvancementEntry> exporter, ResourceCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must add at least one condition.");
return advancement -> {
FabricDataGenHelper.addConditions(advancement, conditions);
@ -94,7 +94,7 @@ public abstract class FabricAdvancementProvider implements DataProvider {
}
JsonObject advancementJson = Advancement.CODEC.encodeStart(ops, advancement.value()).getOrThrow(IllegalStateException::new).getAsJsonObject();
ConditionJsonProvider.write(advancementJson, FabricDataGenHelper.consumeConditions(advancement));
FabricDataGenHelper.addConditions(advancementJson, FabricDataGenHelper.consumeConditions(advancement));
futures.add(DataProvider.writeToPath(writer, advancementJson, getOutputPath(advancement)));
}

View file

@ -27,7 +27,7 @@ import net.minecraft.loot.LootTable;
import net.minecraft.registry.RegistryKey;
import net.fabricmc.fabric.api.datagen.v1.loot.FabricBlockLootTableGenerator;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
/**
@ -44,7 +44,7 @@ public interface FabricLootTableProvider extends LootTableGenerator, DataProvide
*
* <p>For block loot tables, use {@link FabricBlockLootTableGenerator#withConditions} instead.
*/
default BiConsumer<RegistryKey<LootTable>, LootTable.Builder> withConditions(BiConsumer<RegistryKey<LootTable>, LootTable.Builder> exporter, ConditionJsonProvider... conditions) {
default BiConsumer<RegistryKey<LootTable>, LootTable.Builder> withConditions(BiConsumer<RegistryKey<LootTable>, LootTable.Builder> exporter, ResourceCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must add at least one condition.");
return (id, table) -> {
FabricDataGenHelper.addConditions(table, conditions);

View file

@ -44,7 +44,7 @@ import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
/**
@ -69,7 +69,7 @@ public abstract class FabricRecipeProvider extends RecipeProvider {
/**
* Return a new exporter that applies the specified conditions to any recipe json provider it receives.
*/
protected RecipeExporter withConditions(RecipeExporter exporter, ConditionJsonProvider... conditions) {
protected RecipeExporter withConditions(RecipeExporter exporter, ResourceCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must add at least one condition.");
return new RecipeExporter() {
@Override
@ -100,14 +100,14 @@ public abstract class FabricRecipeProvider extends RecipeProvider {
RegistryOps<JsonElement> registryOps = wrapperLookup.getOps(JsonOps.INSTANCE);
JsonObject recipeJson = Recipe.CODEC.encodeStart(registryOps, recipe).getOrThrow(IllegalStateException::new).getAsJsonObject();
ConditionJsonProvider[] conditions = FabricDataGenHelper.consumeConditions(recipe);
ConditionJsonProvider.write(recipeJson, conditions);
ResourceCondition[] conditions = FabricDataGenHelper.consumeConditions(recipe);
FabricDataGenHelper.addConditions(recipeJson, conditions);
list.add(DataProvider.writeToPath(writer, recipeJson, recipesPathResolver.resolveJson(identifier)));
if (advancement != null) {
JsonObject advancementJson = Advancement.CODEC.encodeStart(registryOps, advancement.value()).getOrThrow(IllegalStateException::new).getAsJsonObject();
ConditionJsonProvider.write(advancementJson, conditions);
FabricDataGenHelper.addConditions(advancementJson, conditions);
list.add(DataProvider.writeToPath(writer, advancementJson, advancementsPathResolver.resolveJson(getRecipeIdentifier(advancement.id()))));
}
}

View file

@ -19,6 +19,7 @@ package net.fabricmc.fabric.impl.datagen;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@ -27,7 +28,9 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import com.google.gson.JsonObject;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.apache.commons.lang3.ArrayUtils;
@ -47,7 +50,8 @@ import net.minecraft.util.Util;
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.entrypoint.EntrypointContainer;
@ -215,14 +219,31 @@ public final class FabricDataGenHelper {
/**
* Used to keep track of conditions associated to generated objects.
*/
private static final Map<Object, ConditionJsonProvider[]> CONDITIONS_MAP = new IdentityHashMap<>();
private static final Map<Object, ResourceCondition[]> CONDITIONS_MAP = new IdentityHashMap<>();
public static void addConditions(Object object, ConditionJsonProvider[] conditions) {
public static void addConditions(Object object, ResourceCondition[] conditions) {
CONDITIONS_MAP.merge(object, conditions, ArrayUtils::addAll);
}
@Nullable
public static ConditionJsonProvider[] consumeConditions(Object object) {
public static ResourceCondition[] consumeConditions(Object object) {
return CONDITIONS_MAP.remove(object);
}
/**
* Adds {@code conditions} to {@code baseObject}.
* @param baseObject the base JSON object to which the conditions are inserted
* @param conditions the conditions to insert
* @throws IllegalArgumentException if the object already has conditions
*/
public static void addConditions(JsonObject baseObject, ResourceCondition... conditions) {
if (baseObject.has(ResourceConditions.CONDITIONS_KEY)) {
throw new IllegalArgumentException("Object already has a condition entry: " + baseObject);
} else if (conditions == null || conditions.length == 0) {
// Datagen might pass null conditions.
return;
}
baseObject.add(ResourceConditions.CONDITIONS_KEY, ResourceCondition.LIST_CODEC.encodeStart(JsonOps.INSTANCE, Arrays.asList(conditions)).getOrThrow());
}
}

View file

@ -23,14 +23,14 @@ import net.minecraft.data.server.loottable.BlockLootTableGenerator;
import net.minecraft.loot.LootTable;
import net.minecraft.resource.featuretoggle.FeatureFlags;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
public class ConditionBlockLootTableGenerator extends BlockLootTableGenerator {
private final BlockLootTableGenerator parent;
private final ConditionJsonProvider[] conditions;
private final ResourceCondition[] conditions;
public ConditionBlockLootTableGenerator(BlockLootTableGenerator parent, ConditionJsonProvider[] conditions) {
public ConditionBlockLootTableGenerator(BlockLootTableGenerator parent, ResourceCondition[] conditions) {
super(Collections.emptySet(), FeatureFlags.FEATURE_MANAGER.getFeatureSet());
this.parent = parent;

View file

@ -41,7 +41,7 @@ import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricBlockLootTableProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricLootTableProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.SimpleFabricLootTableProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
public final class FabricLootTableProviderImpl {
@ -55,11 +55,11 @@ public final class FabricLootTableProviderImpl {
FabricDataOutput fabricDataOutput,
CompletableFuture<RegistryWrapper.WrapperLookup> registryLookup) {
HashMap<Identifier, LootTable> builders = Maps.newHashMap();
HashMap<Identifier, ConditionJsonProvider[]> conditionMap = new HashMap<>();
HashMap<Identifier, ResourceCondition[]> conditionMap = new HashMap<>();
return registryLookup.thenCompose(lookup -> {
provider.accept(lookup, (registryKey, builder) -> {
ConditionJsonProvider[] conditions = FabricDataGenHelper.consumeConditions(builder);
ResourceCondition[] conditions = FabricDataGenHelper.consumeConditions(builder);
conditionMap.put(registryKey.getValue(), conditions);
if (builders.put(registryKey.getValue(), builder.type(lootContextType).build()) != null) {
@ -72,7 +72,7 @@ public final class FabricLootTableProviderImpl {
for (Map.Entry<Identifier, LootTable> entry : builders.entrySet()) {
JsonObject tableJson = (JsonObject) LootTable.CODEC.encodeStart(ops, entry.getValue()).getOrThrow(IllegalStateException::new);
ConditionJsonProvider.write(tableJson, conditionMap.remove(entry.getKey()));
FabricDataGenHelper.addConditions(tableJson, conditionMap.remove(entry.getKey()));
futures.add(DataProvider.writeToPath(writer, tableJson, getOutputPath(fabricDataOutput, entry.getKey())));
}

View file

@ -43,7 +43,6 @@ import net.minecraft.advancement.AdvancementFrame;
import net.minecraft.advancement.criterion.OnKilledCriterion;
import net.minecraft.block.Block;
import net.minecraft.block.BlockKeys;
import net.minecraft.block.Blocks;
import net.minecraft.component.ComponentChanges;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.data.DataOutput;
@ -66,6 +65,7 @@ import net.minecraft.loot.provider.number.ConstantLootNumberProvider;
import net.minecraft.recipe.Ingredient;
import net.minecraft.recipe.book.RecipeCategory;
import net.minecraft.registry.Registerable;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryBuilder;
import net.minecraft.registry.RegistryEntryLookup;
import net.minecraft.registry.RegistryKey;
@ -97,13 +97,13 @@ import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.SimpleFabricLootTableProvider;
import net.fabricmc.fabric.api.recipe.v1.ingredient.DefaultCustomIngredients;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.DefaultResourceConditions;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
import net.fabricmc.loader.api.FabricLoader;
public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
private static final ConditionJsonProvider NEVER_LOADED = DefaultResourceConditions.allModsLoaded("a");
private static final ConditionJsonProvider ALWAYS_LOADED = DefaultResourceConditions.not(NEVER_LOADED);
private static final ResourceCondition ALWAYS_LOADED = ResourceConditions.alwaysTrue();
private static final ResourceCondition NEVER_LOADED = ResourceConditions.not(ALWAYS_LOADED);
@Override
public void addJsonKeySortOrders(JsonKeySortOrderCallback callback) {
@ -166,10 +166,10 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.DIAMOND_ORE, 4).input(Items.ITEM_FRAME)
.criterion("has_frame", conditionsFromItem(Items.ITEM_FRAME))
.offerTo(withConditions(exporter, DefaultResourceConditions.itemsRegistered(Blocks.DIAMOND_BLOCK)));
.offerTo(withConditions(exporter, ResourceConditions.registryContains(RegistryKeys.ITEM, Registries.ITEM.getId(Items.DIAMOND_BLOCK))));
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.EMERALD, 4).input(Items.ITEM_FRAME, 2)
.criterion("has_frame", conditionsFromItem(Items.ITEM_FRAME))
.offerTo(withConditions(exporter, DefaultResourceConditions.registryContains(BiomeKeys.PLAINS, BiomeKeys.BADLANDS)));
.offerTo(withConditions(exporter, ResourceConditions.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));
@ -390,7 +390,7 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
@Override
public void generate() {
// Same condition twice to test recursive condition adding
withConditions(ALWAYS_LOADED).withConditions(DefaultResourceConditions.not(NEVER_LOADED)).addDrop(SIMPLE_BLOCK);
withConditions(ALWAYS_LOADED).withConditions(ResourceConditions.not(NEVER_LOADED)).addDrop(SIMPLE_BLOCK);
addDrop(BLOCK_WITHOUT_ITEM, drops(SIMPLE_BLOCK));
excludeFromStrictValidation(BLOCK_WITHOUT_LOOT_TABLE);

View file

@ -1,3 +1,7 @@
version = getSubprojectVersion(project)
loom {
accessWidenerPath = file("src/main/resources/fabric-resource-conditions-api-v1.accesswidener")
}
testDependencies(project, [':fabric-gametest-api-v1'])

View file

@ -1,74 +0,0 @@
/*
* 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.resource.conditions.v1;
import java.util.Objects;
import com.google.common.base.Preconditions;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.Nullable;
import net.minecraft.util.Identifier;
/**
* A resource condition and its parameters that can be serialized to JSON, meant for use in data generators.
*/
public interface ConditionJsonProvider {
/**
* Write the passed conditions to a JSON object in the {@value ResourceConditions#CONDITIONS_KEY} array.
*
* @throws IllegalArgumentException if the JSON object already contains that array
*/
static void write(JsonObject conditionalObject, ConditionJsonProvider @Nullable... conditions) {
if (conditions == null) { // no condition -> skip
return;
}
Preconditions.checkArgument(conditions.length > 0, "Must write at least one condition."); // probably a programmer error
if (conditionalObject.has(ResourceConditions.CONDITIONS_KEY)) throw new IllegalArgumentException("Object already has a condition entry: " + conditionalObject);
JsonArray array = new JsonArray();
for (ConditionJsonProvider condition : conditions) {
Objects.requireNonNull(condition, "condition cannot be null");
array.add(condition.toJson());
}
conditionalObject.add(ResourceConditions.CONDITIONS_KEY, array);
}
/**
* Serialize this condition and its parameters to a new JSON object.
*/
default JsonObject toJson() {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty(ResourceConditions.CONDITION_ID_KEY, getConditionId().toString());
this.writeParameters(jsonObject);
return jsonObject;
}
/**
* {@return the identifier of this condition} This is only for use by {@link #toJson()} to write it.
*/
Identifier getConditionId();
/**
* Write the condition parameters (everything except the {@code "condition": ...} entry). This is only for use by {@link #toJson()}.
*/
void writeParameters(JsonObject object);
}

View file

@ -1,260 +0,0 @@
/*
* 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.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;
import net.minecraft.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
/**
* Contains {@link ConditionJsonProvider}s for resource conditions provided by fabric itself.
*/
public final class DefaultResourceConditions {
private static final Identifier NOT = new Identifier("fabric:not");
private static final Identifier AND = new Identifier("fabric:and");
private static final Identifier OR = new Identifier("fabric:or");
private static final Identifier ALL_MODS_LOADED = new Identifier("fabric:all_mods_loaded");
private static final Identifier ANY_MOD_LOADED = new Identifier("fabric:any_mod_loaded");
private static final Identifier BLOCK_TAGS_POPULATED = new Identifier("fabric:block_tags_populated");
private static final Identifier FLUID_TAGS_POPULATED = new Identifier("fabric:fluid_tags_populated");
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.
*
* @apiNote This condition's ID is {@code fabric:not}, and takes one property, {@code value},
* which is a condition.
*/
public static ConditionJsonProvider not(ConditionJsonProvider value) {
return new ConditionJsonProvider() {
@Override
public void writeParameters(JsonObject object) {
object.add("value", value.toJson());
}
@Override
public Identifier getConditionId() {
return NOT;
}
};
}
/**
* Creates a condition that returns true if all of its child conditions are true.
*
* @apiNote This condition's ID is {@code fabric:and}, and takes one property, {@code values},
* which is an array of conditions.
*/
public static ConditionJsonProvider and(ConditionJsonProvider... values) {
return ResourceConditionsImpl.array(AND, values);
}
/**
* Creates a condition that returns true if any of its child conditions are true.
*
* @apiNote This condition's ID is {@code fabric:or}, and takes one property, {@code values},
* which is an array of conditions.
*/
public static ConditionJsonProvider or(ConditionJsonProvider... values) {
return ResourceConditionsImpl.array(OR, values);
}
/**
* Creates a condition that returns true if all the passed mod ids correspond to a loaded mod.
*
* @apiNote This condition's ID is {@code fabric:all_mods_loaded}, and takes one property,
* {@code values}, which is an array of string mod IDs.
*/
public static ConditionJsonProvider allModsLoaded(String... modIds) {
return ResourceConditionsImpl.mods(ALL_MODS_LOADED, modIds);
}
/**
* Creates a condition that returns true if at least one of the passed mod ids corresponds to a loaded mod.
*
* @apiNote This condition's ID is {@code fabric:any_mod_loaded}, and takes one property,
* {@code values}, which is an array of string mod IDs.
*/
public static ConditionJsonProvider anyModLoaded(String... modIds) {
return ResourceConditionsImpl.mods(ANY_MOD_LOADED, modIds);
}
/**
* Create a condition that returns true if each of the passed block tags exists and has at least one element.
* @deprecated Use {@link #tagsPopulated} instead.
*/
@SafeVarargs
@Deprecated
public static ConditionJsonProvider blockTagsPopulated(TagKey<Block>... tags) {
return ResourceConditionsImpl.tagsPopulated(BLOCK_TAGS_POPULATED, false, tags);
}
/**
* Create a condition that returns true if each of the passed fluid tags exists and has at least one element.
* @deprecated Use {@link #tagsPopulated} instead.
*/
@SafeVarargs
@Deprecated
public static ConditionJsonProvider fluidTagsPopulated(TagKey<Fluid>... tags) {
return ResourceConditionsImpl.tagsPopulated(FLUID_TAGS_POPULATED, false, tags);
}
/**
* Create a condition that returns true if each of the passed item tags exists and has at least one element.
* @deprecated Use {@link #tagsPopulated} instead.
*/
@SafeVarargs
@Deprecated
public static ConditionJsonProvider itemTagsPopulated(TagKey<Item>... tags) {
return ResourceConditionsImpl.tagsPopulated(ITEM_TAGS_POPULATED, false, tags);
}
/**
* Creates a condition that returns true if each of the passed tags exists and has at least one element.
* This works for any registries, and the registry ID of the tags is serialized to JSON as well as the tags.
*
* @apiNote This condition's ID is {@code fabric:tags_populated}, and takes up to two properties:
* {@code values}, which is an array of string tag IDs, and {@code registry}, which is the ID of
* the registry of the tags. If {@code registry} is not provided, it defaults to {@code minecraft:item}.
*/
@SafeVarargs
public static <T> ConditionJsonProvider tagsPopulated(TagKey<T>... tags) {
return ResourceConditionsImpl.tagsPopulated(TAGS_POPULATED, true, tags);
}
/**
* Creates a condition that returns true if all the passed features are enabled.
* @param features the features to check for
*
* @apiNote This condition's ID is {@code fabric:features_enabled}, and takes one property:
* {@code features}, which is the array of the IDs of the feature flag to check.
*/
public static ConditionJsonProvider featuresEnabled(FeatureFlag... features) {
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
}
static {
ResourceConditions.register(NOT, object -> {
JsonObject condition = JsonHelper.getObject(object, "value");
return !ResourceConditions.conditionMatches(condition);
});
ResourceConditions.register(AND, object -> {
JsonArray array = JsonHelper.getArray(object, "values");
return ResourceConditions.conditionsMatch(array, true);
});
ResourceConditions.register(OR, object -> {
JsonArray array = JsonHelper.getArray(object, "values");
return ResourceConditions.conditionsMatch(array, false);
});
ResourceConditions.register(ALL_MODS_LOADED, object -> ResourceConditionsImpl.modsLoadedMatch(object, true));
ResourceConditions.register(ANY_MOD_LOADED, object -> ResourceConditionsImpl.modsLoadedMatch(object, false));
ResourceConditions.register(BLOCK_TAGS_POPULATED, object -> ResourceConditionsImpl.tagsPopulatedMatch(object, RegistryKeys.BLOCK));
ResourceConditions.register(FLUID_TAGS_POPULATED, object -> ResourceConditionsImpl.tagsPopulatedMatch(object, RegistryKeys.FLUID));
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() {
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.resource.conditions.v1;
import java.util.List;
import com.mojang.serialization.Codec;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
/**
* A resource condition. To create a custom condition type, implement this interface,
* call {@link ResourceConditionType#create} and create the type, then register
* via {@link ResourceConditions#register}.
*/
public interface ResourceCondition {
/**
* A codec for a resource condition. It is a map with a {@code condition} key denoting the ID
* of the condition, and any additional values required by the specific condition.
*/
Codec<ResourceCondition> CODEC = ResourceConditionType.TYPE_CODEC.dispatch("condition", ResourceCondition::getType, ResourceConditionType::codec);
/**
* A codec for a list of conditions.
*/
Codec<List<ResourceCondition>> LIST_CODEC = CODEC.listOf();
/**
* @return the type of the condition
*/
ResourceConditionType<?> getType();
/**
* Tests the condition. The passed registry lookup, if any, includes all static and dynamic entries.
* However, the tags are not loaded yet.
*
* @implNote {@code registryLookup} should never be {@code null} for supported use cases
* (such as recipes or advancements). However, it may be {@code null} in client-side
* resources, or for non-vanilla resource types.
*
* @param registryLookup the registry lookup, or {@code null} in case registry is unavailable
* @return whether the condition was successful
*/
boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup);
}

View file

@ -0,0 +1,74 @@
/*
* 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.resource.conditions.v1;
import java.util.Objects;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.MapCodec;
import net.minecraft.util.Identifier;
import net.minecraft.util.Nullables;
/**
* A type of resource conditions.
* @param <T> the type of {@link ResourceCondition}
*/
public interface ResourceConditionType<T extends ResourceCondition> {
/**
* A codec used to serialize the condition type.
*/
Codec<ResourceConditionType<?>> TYPE_CODEC = Identifier.CODEC.comapFlatMap(id ->
Nullables.mapOrElseGet(ResourceConditions.getConditionType(id), DataResult::success, () -> DataResult.error(() -> "Unknown resource condition key: "+ id)),
ResourceConditionType::id
);
/**
* @return the condition's ID
*/
Identifier id();
/**
* @return the condition's codec
*/
MapCodec<T> codec();
/**
* Creates a resource condition type. The returned value needs to be registered with {@link ResourceConditions#register}.
* @param id the ID of the condition
* @param codec the codec used to serialize the condition
* @param <T> the type of the resource condition
* @return the condition type to register
*/
static <T extends ResourceCondition> ResourceConditionType<T> create(Identifier id, MapCodec<T> codec) {
Objects.requireNonNull(id, "id cannot be null");
Objects.requireNonNull(codec, "codec cannot be null");
return new ResourceConditionType<>() {
@Override
public Identifier id() {
return id;
}
@Override
public MapCodec<T> codec() {
return codec;
}
};
}
}

View file

@ -16,167 +16,172 @@
package net.fabricmc.fabric.api.resource.conditions.v1;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.jetbrains.annotations.Nullable;
import net.minecraft.resource.JsonDataLoader;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.resource.featuretoggle.FeatureFlag;
import net.minecraft.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
import net.fabricmc.fabric.impl.resource.conditions.conditions.AllModsLoadedResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.AndResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.AnyModsLoadedResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.FeaturesEnabledResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.NotResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.OrResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.RegistryContainsResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.TagsPopulatedResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.TrueResourceCondition;
/**
* Registration and access to resource loading conditions.
* A resource condition is an identified {@code Predicate<JsonObject>} that can decide whether a resource should be loaded or not.
* <ul>
* <li>A JSON object that may contain a condition can be parsed with {@link #objectMatchesConditions}.
* This is the preferred way of implementing conditional objects, as it handles the details of the format (see below) and catches and logs thrown exceptions.
* This function should only be called from the "apply" phase of a {@link net.minecraft.resource.ResourceReloader},
* otherwise some conditions might behave in unexpected ways.
* </li>
* <li>The lower-level {@link #conditionsMatch} and {@link #conditionMatches} may be useful when implementing conditions.</li>
* <li>Conditions are registered with {@link #register} and queried with {@link #get}.</li>
* </ul>
*
* <p>At the moment, Fabric only recognizes conditions for resources loaded by subclasses of {@link JsonDataLoader}.
* This means: recipes, advancements, loot tables, loot functions and loot conditions.
*
* <p>Fabric provides some conditions, which can be generated using the helper methods in {@link DefaultResourceConditions}.
*
* <h3>Details of the format</h3>
*
* <p>A conditional JSON object must have a {@link #CONDITIONS_KEY} entry, containing an array of condition objects.
* The conditions in the array must be satisfied to load the resource.
* Each condition object must contain a {@link #CONDITION_ID_KEY} entry with the identifier of the condition,
* and it may also contain additional data for the condition.
* Here is an example of a resource that is only loaded if no mod with id {@code a} is loaded:
* <pre>{@code
* {
* ... // normal contents of the resource
* "fabric:load_conditions": [ // array of condition objects
* { // a condition object
* // the identifier of the condition... the "fabric:not" condition inverts the condition in its "value" field
* "condition": "fabric:not",
* // additional data, for "fabric:not", the "value" field is required to be another condition object
* "value": {
* // the identifier of the condition
* "condition": "fabric:all_mods_loaded",
* // additional data, for "fabric:all_mods_loaded"
* "values": [
* "a"
* ]
* }
* }
* ]
* }
* }</pre>
* Contains default resource conditions and the condition registry.
*/
public final class ResourceConditions {
private static final Map<Identifier, Predicate<JsonObject>> REGISTERED_CONDITIONS = new ConcurrentHashMap<>();
private static final Map<Identifier, ResourceConditionType<?>> REGISTERED_CONDITIONS = new ConcurrentHashMap<>();
/**
* The key ({@value}) Fabric uses to identify resource conditions in a JSON object.
* The JSON key for resource conditions, {@value #CONDITIONS_KEY}.
*/
public static final String CONDITIONS_KEY = "fabric:load_conditions";
/**
* The key ({@value}) identifying the resource condition's identifier inside a condition object.
*/
public static final String CONDITION_ID_KEY = "condition";
/**
* Register a new resource condition.
*
* @throws IllegalArgumentException If a resource condition is already registered with the same name.
*/
public static void register(Identifier identifier, Predicate<JsonObject> condition) {
Objects.requireNonNull(identifier, "Identifier may not be null.");
Objects.requireNonNull(condition, "Condition may not be null.");
if (REGISTERED_CONDITIONS.put(identifier, condition) != null) {
throw new IllegalArgumentException("Duplicate JSON condition registration with id " + identifier);
}
}
/**
* Get the resource condition with the passed name, or {@code null} if none is registered (yet).
*/
@Nullable
public static Predicate<JsonObject> get(Identifier identifier) {
return REGISTERED_CONDITIONS.get(identifier);
}
/**
* Check if the passed JSON object either has no {@code fabric:conditions} tag, or all of its conditions match.
* This should be called for objects that may contain a conditions entry.
*
* <p>This function should only be called from the "apply" phase of a {@link net.minecraft.resource.ResourceReloader},
* otherwise some conditions might behave in unexpected ways.
*
* <p>If an exception is thrown during condition testing, it will be caught and logged, and false will be returned.
*/
public static boolean objectMatchesConditions(JsonObject object) {
try {
JsonArray conditions = JsonHelper.getArray(object, CONDITIONS_KEY, null);
if (conditions == null) {
return true; // no conditions
} else {
return conditionsMatch(conditions, true);
}
} catch (RuntimeException exception) {
ResourceConditionsImpl.LOGGER.warn("Skipping object %s. Failed to parse resource conditions".formatted(object), exception);
return false;
}
}
/**
* If {@code and} is true, check if all the passed conditions match.
* If it is false, check if at least one of the passed conditions matches.
*
* @throws RuntimeException If some condition failed to parse.
*/
public static boolean conditionsMatch(JsonArray conditions, boolean and) throws RuntimeException {
for (JsonElement element : conditions) {
if (element.isJsonObject()) {
if (conditionMatches(element.getAsJsonObject()) != and) {
return !and;
}
} else {
throw new JsonParseException("Invalid condition entry: " + element);
}
}
return and;
}
/**
* Check if the passed condition object matches.
*
* @throws RuntimeException If some condition failed to parse.
*/
public static boolean conditionMatches(JsonObject condition) throws RuntimeException {
Identifier conditionId = new Identifier(JsonHelper.getString(condition, CONDITION_ID_KEY));
Predicate<JsonObject> jrc = get(conditionId);
if (jrc == null) {
throw new JsonParseException("Unknown recipe condition: " + conditionId);
} else {
return jrc.test(condition);
}
}
private ResourceConditions() {
}
static {
// Load Fabric-provided conditions.
DefaultResourceConditions.init();
/**
* Registers {@code condition}.
* @param condition the condition to register
* @throws IllegalArgumentException if {@code condition} is already registered
*/
public static void register(ResourceConditionType<?> condition) {
Objects.requireNonNull(condition, "Condition may not be null.");
if (REGISTERED_CONDITIONS.put(condition.id(), condition) != null) {
throw new IllegalArgumentException("Duplicate resource condition registered with id " + condition.id());
}
}
/**
* @return the condition with ID {@code id}, or {@code null} if there is no such condition
*/
public static ResourceConditionType<?> getConditionType(Identifier id) {
return REGISTERED_CONDITIONS.get(id);
}
/**
* A condition that always passes. Has ID {@code fabric:true}.
*/
public static ResourceCondition alwaysTrue() {
return new TrueResourceCondition();
}
/**
* A condition that passes if {@code condition} does not pass. Has ID {@code fabric:not} and
* takes one field, {@code value}, which is a resource condition.
*/
public static ResourceCondition not(ResourceCondition condition) {
return new NotResourceCondition(condition);
}
/**
* A condition that passes if each of the {@code conditions} passes. Has ID {@code fabric:and}
* and takes one field, {@code values}, which is a list of resource conditions.
* If there are no conditions to check, it always passes.
*/
public static ResourceCondition and(ResourceCondition... conditions) {
return new AndResourceCondition(List.of(conditions));
}
/**
* A condition that passes if any of the {@code conditions} passes. Has ID {@code fabric:or}
* and takes one field, {@code values}, which is a list of resource conditions.
* If there are no conditions to check, it always fails.
*/
public static ResourceCondition or(ResourceCondition... conditions) {
return new OrResourceCondition(List.of(conditions));
}
/**
* A condition that passes if each of the specified {@code modIds} are loaded. Has ID
* {@code all_mods_loaded} and takes one field, {@code values}, which is a list of strings indicating
* mod IDs. If there are no IDs to check, it always passes.
*/
public static ResourceCondition allModsLoaded(String... modIds) {
return new AllModsLoadedResourceCondition(List.of(modIds));
}
/**
* A condition that passes if any of the specified {@code modIds} are loaded. Has ID
* {@code any_mods_loaded} and takes one field, {@code values}, which is a list of strings indicating
* mod IDs. If there are no IDs to check, it always fails.
*/
public static ResourceCondition anyModsLoaded(String... modIds) {
return new AnyModsLoadedResourceCondition(List.of(modIds));
}
/**
* A condition that passes if each of the {@code tags} exist. This does not check if those tags have
* any entries. Has ID {@code tags_populated} and takes two fields: {@code registry}, which is the ID
* of the registry the tag is for, and {@code values}, which is a list of the tag IDs to check.
* If there are no IDs to check, it always passes, including in cases where a nonexistent registry is
* specified.
*
* @implNote Because tags are loaded after loot tables (and predicates/item modifiers), these resources
* do not support this condition. In those cases, this condition logs a warning and always fails.
* @param <T> the type of the tag values
*/
@SafeVarargs
public static <T> ResourceCondition tagsPopulated(TagKey<T>... tags) {
return new TagsPopulatedResourceCondition(tags);
}
/**
* @see #tagsPopulated(TagKey[])
* @param <T> the type of the tag values
*/
@SafeVarargs
public static <T> ResourceCondition tagsPopulated(RegistryKey<? extends Registry<T>> registry, TagKey<T>... tags) {
return new TagsPopulatedResourceCondition(registry.getValue(), tags);
}
/**
* A condition that passes if each of the {@code features} are enabled. Has ID {@code features_enabled}
* and takes one field, {@code features}, which is a list of the feature IDs. If there are no IDs to
* check, it always passes. If an unknown feature is specified, it always fails.
*/
public static ResourceCondition featuresEnabled(Identifier... features) {
return new FeaturesEnabledResourceCondition(features);
}
/**
* @see #featuresEnabled(Identifier...)
*/
public static ResourceCondition featuresEnabled(FeatureFlag... features) {
return new FeaturesEnabledResourceCondition(features);
}
/**
* A condition that passes if each of the {@code entries} exist. The entries may be from static or
* dynamic registries. Has ID {@code registry_contains} and takes two fields: {@code registry}, which
* is the ID of the registry, and {@code values}, which is a list of IDs to check.
* If there are no IDs to check, it always passes, including in cases where a nonexistent registry is
* specified.
*
* @param <T> the type of the tag values
*/
@SafeVarargs
public static <T> ResourceCondition registryContains(RegistryKey<T>... entries) {
return new RegistryContainsResourceCondition(entries);
}
/**
* @see #registryContains(RegistryKey[])
* @param <T> the type of the tag values
*/
public static <T> ResourceCondition registryContains(RegistryKey<? extends Registry<T>> registry, Identifier... entries) {
return new RegistryContainsResourceCondition(registry.getValue(), entries);
}
}

View file

@ -16,7 +16,7 @@
/**
* Provides a way of conditionally loading JSON-based resources. By default, this can
* be used with recipes, loot tables, advancements, predicates, and item modifiers.
* be used with recipes, advancements, loot tables, predicates, and item modifiers.
* Conditions are identified by an identifier and registered at {@link
* net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions}.
*
@ -43,13 +43,12 @@
* }
* }</pre>
*
* <p>See {@link net.fabricmc.fabric.api.resource.conditions.v1.DefaultResourceConditions} for
* the list of built-in conditions. It is also possible to register a custom condition.
* <p>Unknown/invalid conditions will be skipped and considered successful.
*
* <h2>Data generation integration</h2>
*
* <p>Fabric Data Generation API supports adding a {@link
* net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider} to a generated file.
* net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition} to a generated file.
* Please check the documentation of the Data Generation API.
*/
package net.fabricmc.fabric.api.resource.conditions.v1;

View file

@ -0,0 +1,49 @@
/*
* 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.resource.conditions;
import com.mojang.serialization.MapCodec;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.conditions.AllModsLoadedResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.AndResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.AnyModsLoadedResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.FeaturesEnabledResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.NotResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.OrResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.RegistryContainsResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.TagsPopulatedResourceCondition;
import net.fabricmc.fabric.impl.resource.conditions.conditions.TrueResourceCondition;
public class DefaultResourceConditionTypes {
public static final ResourceConditionType<TrueResourceCondition> TRUE = createResourceConditionType("true", TrueResourceCondition.CODEC);
public static final ResourceConditionType<NotResourceCondition> NOT = createResourceConditionType("not", NotResourceCondition.CODEC);
public static final ResourceConditionType<OrResourceCondition> OR = createResourceConditionType("or", OrResourceCondition.CODEC);
public static final ResourceConditionType<AndResourceCondition> AND = createResourceConditionType("and", AndResourceCondition.CODEC);
public static final ResourceConditionType<AllModsLoadedResourceCondition> ALL_MODS_LOADED = createResourceConditionType("all_mods_loaded", AllModsLoadedResourceCondition.CODEC);
public static final ResourceConditionType<AnyModsLoadedResourceCondition> ANY_MODS_LOADED = createResourceConditionType("any_mods_loaded", AnyModsLoadedResourceCondition.CODEC);
public static final ResourceConditionType<TagsPopulatedResourceCondition> TAGS_POPULATED = createResourceConditionType("tags_populated", TagsPopulatedResourceCondition.CODEC);
public static final ResourceConditionType<FeaturesEnabledResourceCondition> FEATURES_ENABLED = createResourceConditionType("features_enabled", FeaturesEnabledResourceCondition.CODEC);
public static final ResourceConditionType<RegistryContainsResourceCondition> REGISTRY_CONTAINS = createResourceConditionType("registry_contains", RegistryContainsResourceCondition.CODEC);
private static <T extends ResourceCondition> ResourceConditionType<T> createResourceConditionType(String name, MapCodec<T> codec) {
return ResourceConditionType.create(new Identifier("fabric", name), codec);
}
}

View file

@ -17,178 +17,89 @@
package net.fabricmc.fabric.impl.resource.conditions;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import com.google.common.base.Preconditions;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.JsonOps;
import org.apache.commons.lang3.mutable.MutableBoolean;
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;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.tag.TagManagerLoader;
import net.minecraft.resource.featuretoggle.FeatureFlag;
import net.minecraft.resource.featuretoggle.FeatureFlags;
import net.minecraft.resource.featuretoggle.FeatureSet;
import net.minecraft.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
import net.fabricmc.loader.api.FabricLoader;
public final class ResourceConditionsImpl {
public final class ResourceConditionsImpl implements ModInitializer {
public static final Logger LOGGER = LoggerFactory.getLogger("Fabric Resource Conditions");
public static FeatureSet currentFeatures = null;
// Providers
public static ConditionJsonProvider array(Identifier id, ConditionJsonProvider... values) {
Preconditions.checkArgument(values.length > 0, "Must register at least one value.");
return new ConditionJsonProvider() {
@Override
public Identifier getConditionId() {
return id;
}
@Override
public void writeParameters(JsonObject object) {
JsonArray array = new JsonArray();
for (ConditionJsonProvider provider : values) {
array.add(provider.toJson());
}
object.add("values", array);
}
};
@Override
public void onInitialize() {
ResourceConditions.register(DefaultResourceConditionTypes.TRUE);
ResourceConditions.register(DefaultResourceConditionTypes.NOT);
ResourceConditions.register(DefaultResourceConditionTypes.AND);
ResourceConditions.register(DefaultResourceConditionTypes.OR);
ResourceConditions.register(DefaultResourceConditionTypes.ALL_MODS_LOADED);
ResourceConditions.register(DefaultResourceConditionTypes.ANY_MODS_LOADED);
ResourceConditions.register(DefaultResourceConditionTypes.TAGS_POPULATED);
ResourceConditions.register(DefaultResourceConditionTypes.FEATURES_ENABLED);
ResourceConditions.register(DefaultResourceConditionTypes.REGISTRY_CONTAINS);
}
public static ConditionJsonProvider mods(Identifier id, String... modIds) {
Preconditions.checkArgument(modIds.length > 0, "Must register at least one mod id.");
public static boolean applyResourceConditions(JsonObject obj, String dataType, Identifier key, @Nullable RegistryWrapper.WrapperLookup registryLookup) {
boolean debugLogEnabled = ResourceConditionsImpl.LOGGER.isDebugEnabled();
return new ConditionJsonProvider() {
@Override
public Identifier getConditionId() {
return id;
}
if (obj.has(ResourceConditions.CONDITIONS_KEY)) {
DataResult<List<ResourceCondition>> conditions = ResourceCondition.LIST_CODEC.parse(JsonOps.INSTANCE, obj.get(ResourceConditions.CONDITIONS_KEY));
@Override
public void writeParameters(JsonObject object) {
JsonArray array = new JsonArray();
if (conditions.isSuccess()) {
boolean matched = ResourceConditionsImpl.conditionsMet(conditions.getOrThrow(), registryLookup, true);
for (String modId : modIds) {
array.add(modId);
if (debugLogEnabled) {
String verdict = matched ? "Allowed" : "Rejected";
ResourceConditionsImpl.LOGGER.debug("{} resource of type {} with id {}", verdict, dataType, key);
}
object.add("values", array);
return matched;
} else {
ResourceConditionsImpl.LOGGER.error("Failed to parse resource conditions for file of type {} with id {}, skipping: {}", dataType, key, conditions.error().get().message());
}
};
}
}
@SafeVarargs
public static <T> ConditionJsonProvider tagsPopulated(Identifier id, boolean includeRegistry, TagKey<T>... tags) {
Preconditions.checkArgument(tags.length > 0, "Must register at least one tag.");
final RegistryKey<? extends Registry<?>> registryRef = tags[0].registry();
return new ConditionJsonProvider() {
@Override
public Identifier getConditionId() {
return id;
}
@Override
public void writeParameters(JsonObject object) {
JsonArray array = new JsonArray();
for (TagKey<T> tag : tags) {
array.add(tag.id().toString());
}
object.add("values", array);
if (includeRegistry && registryRef != RegistryKeys.ITEM) {
// tags[0] is guaranteed to exist.
// Skip if this is the default (minecraft:item)
object.addProperty("registry", registryRef.getValue().toString());
}
}
};
}
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());
}
}
};
return true;
}
// Condition implementations
public static boolean modsLoadedMatch(JsonObject object, boolean and) {
JsonArray array = JsonHelper.getArray(object, "values");
public static boolean conditionsMet(List<ResourceCondition> conditions, @Nullable RegistryWrapper.WrapperLookup registryLookup, boolean and) {
for (ResourceCondition condition : conditions) {
if (condition.test(registryLookup) != and) {
return !and;
}
}
for (JsonElement element : array) {
if (element.isJsonPrimitive()) {
if (FabricLoader.getInstance().isModLoaded(element.getAsString()) != and) {
return !and;
}
} else {
throw new JsonParseException("Invalid mod id entry: " + element);
return and;
}
public static boolean modsLoaded(List<String> modIds, boolean and) {
for (String modId : modIds) {
if (FabricLoader.getInstance().isModLoaded(modId) != and) {
return !and;
}
}
@ -200,108 +111,78 @@ public final class ResourceConditionsImpl {
* The tags are set at the end of the "apply" phase in {@link TagManagerLoader}, and cleared in {@link net.minecraft.server.DataPackContents#refresh}.
* If the resource reload fails, the thread local is not cleared and:
* - the map will remain in memory until the next reload;
* - any call to {@link #tagsPopulatedMatch} will check the tags from the failed reload instead of failing directly.
* - any call to {@link #tagsPopulated} will check the tags from the failed reload instead of failing directly.
* This is probably acceptable.
*/
public static final ThreadLocal<Map<RegistryKey<?>, Map<Identifier, Collection<RegistryEntry<?>>>>> LOADED_TAGS = new ThreadLocal<>();
public static final ThreadLocal<Map<RegistryKey<?>, Set<Identifier>>> LOADED_TAGS = new ThreadLocal<>();
@SuppressWarnings({"unchecked", "rawtypes"})
public static void setTags(List<TagManagerLoader.RegistryTags<?>> tags) {
Map<RegistryKey<?>, Map<Identifier, Collection<RegistryEntry<?>>>> tagMap = new HashMap<>();
Map<RegistryKey<?>, Set<Identifier>> tagMap = new IdentityHashMap<>();
for (TagManagerLoader.RegistryTags<?> registryTags : tags) {
tagMap.put(registryTags.key(), (Map) registryTags.tags());
tagMap.put(registryTags.key(), registryTags.tags().keySet());
}
LOADED_TAGS.set(tagMap);
}
public static boolean tagsPopulatedMatch(JsonObject object) {
String key = JsonHelper.getString(object, "registry", "minecraft:item");
RegistryKey<? extends Registry<?>> registryRef = RegistryKey.ofRegistry(new Identifier(key));
return tagsPopulatedMatch(object, registryRef);
}
// Cannot use registry because tags are not loaded to the registry at this stage yet.
public static boolean tagsPopulated(Identifier registryId, List<Identifier> tags) {
Map<RegistryKey<?>, Set<Identifier>> tagMap = LOADED_TAGS.get();
public static boolean tagsPopulatedMatch(JsonObject object, RegistryKey<? extends Registry<?>> registryKey) {
JsonArray array = JsonHelper.getArray(object, "values");
@Nullable
Map<RegistryKey<?>, Map<Identifier, Collection<RegistryEntry<?>>>> allTags = LOADED_TAGS.get();
if (allTags == null) {
LOGGER.warn("Can't retrieve deserialized tags. Failing tags_populated resource condition check.");
if (tagMap == null) {
LOGGER.warn("Can't retrieve registry {}, failing tags_populated resource condition check", registryId);
return false;
}
Map<Identifier, Collection<RegistryEntry<?>>> registryTags = allTags.get(registryKey);
Set<Identifier> tagSet = tagMap.get(RegistryKey.ofRegistry(registryId));
if (registryTags == null) {
// No tag for this registry
return array.isEmpty();
if (tagSet == null) {
return tags.isEmpty();
} else {
return tagSet.containsAll(tags);
}
for (JsonElement element : array) {
if (element.isJsonPrimitive()) {
Identifier id = new Identifier(element.getAsString());
Collection<RegistryEntry<?>> tags = registryTags.get(id);
if (tags == null || tags.isEmpty()) {
return false;
}
} else {
throw new JsonParseException("Invalid tag id entry: " + element);
}
}
return true;
}
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();
FeatureSet set = FeatureFlags.FEATURE_MANAGER.featureSetOf(featureIds, (id) -> {
throw new JsonParseException("Unknown feature flag: " + id);
public static boolean featuresEnabled(Collection<Identifier> features) {
MutableBoolean foundUnknown = new MutableBoolean();
FeatureSet set = FeatureFlags.FEATURE_MANAGER.featureSetOf(features, (id) -> {
LOGGER.info("Found unknown feature {}, treating it as failure", id);
foundUnknown.setTrue();
});
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.");
if (foundUnknown.booleanValue()) {
return false;
}
Optional<Registry<E>> registry = registries.getOptional(registryRef);
if (registry.isEmpty()) {
// No such registry
return array.isEmpty();
if (currentFeatures == null) {
LOGGER.warn("Can't retrieve current features, failing features_enabled resource condition check.");
return false;
}
for (JsonElement element : array) {
if (element.isJsonPrimitive()) {
Identifier id = new Identifier(element.getAsString());
return set.isSubsetOf(currentFeatures);
}
if (!registry.get().containsId(id)) {
public static boolean registryContains(@Nullable RegistryWrapper.WrapperLookup registryLookup, Identifier registryId, List<Identifier> entries) {
RegistryKey<? extends Registry<Object>> registryKey = RegistryKey.ofRegistry(registryId);
if (registryLookup == null) {
LOGGER.warn("Can't retrieve registry {}, failing registry_contains resource condition check", registryId);
return false;
}
Optional<RegistryWrapper.Impl<Object>> wrapper = registryLookup.getOptionalWrapper(registryKey);
if (wrapper.isPresent()) {
for (Identifier id : entries) {
if (wrapper.get().getOptional(RegistryKey.of(registryKey, id)).isEmpty()) {
return false;
}
} else {
throw new JsonParseException("Invalid registry entry id: " + element);
}
}
return true;
return true;
} else {
return entries.isEmpty();
}
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.resource.conditions.conditions;
import java.util.List;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public record AllModsLoadedResourceCondition(List<String> modIds) implements ResourceCondition {
public static final MapCodec<AllModsLoadedResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.STRING.listOf().fieldOf("values").forGetter(AllModsLoadedResourceCondition::modIds)
).apply(instance, AllModsLoadedResourceCondition::new));
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.ALL_MODS_LOADED;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return ResourceConditionsImpl.modsLoaded(this.modIds(), true);
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.resource.conditions.conditions;
import java.util.List;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public record AndResourceCondition(List<ResourceCondition> conditions) implements ResourceCondition {
public static final MapCodec<AndResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ResourceCondition.CODEC.listOf().fieldOf("values").forGetter(AndResourceCondition::conditions)
).apply(instance, AndResourceCondition::new));
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.AND;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return ResourceConditionsImpl.conditionsMet(this.conditions(), registryLookup, true);
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.resource.conditions.conditions;
import java.util.List;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public record AnyModsLoadedResourceCondition(List<String> modIds) implements ResourceCondition {
public static final MapCodec<AnyModsLoadedResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.STRING.listOf().fieldOf("values").forGetter(AnyModsLoadedResourceCondition::modIds)
).apply(instance, AnyModsLoadedResourceCondition::new));
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.ANY_MODS_LOADED;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return ResourceConditionsImpl.modsLoaded(this.modIds(), false);
}
}

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.impl.resource.conditions.conditions;
import java.util.Collection;
import java.util.List;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.resource.featuretoggle.FeatureFlag;
import net.minecraft.resource.featuretoggle.FeatureFlags;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public record FeaturesEnabledResourceCondition(Collection<Identifier> features) implements ResourceCondition {
public static final MapCodec<FeaturesEnabledResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Identifier.CODEC.listOf().fieldOf("features").forGetter(condition -> List.copyOf(condition.features))
).apply(instance, FeaturesEnabledResourceCondition::new));
public FeaturesEnabledResourceCondition(Identifier... features) {
this(List.of(features));
}
public FeaturesEnabledResourceCondition(FeatureFlag... flags) {
this(FeatureFlags.FEATURE_MANAGER.toId(FeatureFlags.FEATURE_MANAGER.featureSetOf(flags)));
}
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.FEATURES_ENABLED;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return ResourceConditionsImpl.featuresEnabled(this.features());
}
}

View file

@ -0,0 +1,43 @@
/*
* 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.resource.conditions.conditions;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
public record NotResourceCondition(ResourceCondition condition) implements ResourceCondition {
public static final MapCodec<NotResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ResourceCondition.CODEC.fieldOf("value").forGetter(NotResourceCondition::condition)
).apply(instance, NotResourceCondition::new));
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.NOT;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return !this.condition().test(registryLookup);
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.resource.conditions.conditions;
import java.util.List;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public record OrResourceCondition(List<ResourceCondition> conditions) implements ResourceCondition {
public static final MapCodec<OrResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ResourceCondition.CODEC.listOf().fieldOf("values").forGetter(OrResourceCondition::conditions)
).apply(instance, OrResourceCondition::new));
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.OR;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return ResourceConditionsImpl.conditionsMet(this.conditions(), registryLookup, false);
}
}

View file

@ -0,0 +1,62 @@
/*
* 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.resource.conditions.conditions;
import java.util.Arrays;
import java.util.List;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public record RegistryContainsResourceCondition(Identifier registry, List<Identifier> entries) implements ResourceCondition {
// Cannot use registry-bound codec because they fail parsing if nonexistent,
// and resource conditions themselves should not fail to parse on condition failure
public static final MapCodec<RegistryContainsResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Identifier.CODEC.fieldOf("registry").orElse(RegistryKeys.ITEM.getValue()).forGetter(RegistryContainsResourceCondition::registry),
Identifier.CODEC.listOf().fieldOf("values").forGetter(RegistryContainsResourceCondition::entries)
).apply(instance, RegistryContainsResourceCondition::new));
public RegistryContainsResourceCondition(Identifier registry, Identifier... entries) {
this(registry, List.of(entries));
}
@SafeVarargs
public <T> RegistryContainsResourceCondition(RegistryKey<T>... entries) {
this(entries[0].getRegistry(), Arrays.stream(entries).map(RegistryKey::getValue).toList());
}
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.REGISTRY_CONTAINS;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return ResourceConditionsImpl.registryContains(registryLookup, this.registry(), this.entries());
}
}

View file

@ -0,0 +1,63 @@
/*
* 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.resource.conditions.conditions;
import java.util.Arrays;
import java.util.List;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public record TagsPopulatedResourceCondition(Identifier registry, List<Identifier> tags) implements ResourceCondition {
// Cannot use registry-bound codec because they fail parsing if nonexistent,
// and resource conditions themselves should not fail to parse on condition failure
public static final MapCodec<TagsPopulatedResourceCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Identifier.CODEC.fieldOf("registry").orElse(RegistryKeys.ITEM.getValue()).forGetter(TagsPopulatedResourceCondition::registry),
Identifier.CODEC.listOf().fieldOf("values").forGetter(TagsPopulatedResourceCondition::tags)
).apply(instance, TagsPopulatedResourceCondition::new));
@SafeVarargs
public <T> TagsPopulatedResourceCondition(Identifier registry, TagKey<T>... tags) {
this(registry, Arrays.stream(tags).map(TagKey::id).toList());
}
@SafeVarargs
public <T> TagsPopulatedResourceCondition(TagKey<T>... tags) {
this(tags[0].registry().getValue(), Arrays.stream(tags).map(TagKey::id).toList());
}
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.TAGS_POPULATED;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return ResourceConditionsImpl.tagsPopulated(this.registry(), this.tags());
}
}

View file

@ -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.impl.resource.conditions.conditions;
import com.mojang.serialization.MapCodec;
import org.jetbrains.annotations.Nullable;
import net.minecraft.registry.RegistryWrapper;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType;
import net.fabricmc.fabric.impl.resource.conditions.DefaultResourceConditionTypes;
public class TrueResourceCondition implements ResourceCondition {
public static final MapCodec<TrueResourceCondition> CODEC = MapCodec.unit(TrueResourceCondition::new);
@Override
public ResourceConditionType<?> getType() {
return DefaultResourceConditionTypes.TRUE;
}
@Override
public boolean test(@Nullable RegistryWrapper.WrapperLookup registryLookup) {
return true;
}
}

View file

@ -28,6 +28,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.registry.CombinedDynamicRegistries;
import net.minecraft.registry.ServerDynamicRegistryType;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.featuretoggle.FeatureFlags;
import net.minecraft.resource.featuretoggle.FeatureSet;
import net.minecraft.server.DataPackContents;
import net.minecraft.server.command.CommandManager;
@ -36,17 +37,12 @@ import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
@Mixin(DataPackContents.class)
public class DataPackContentsMixin {
/**
* Clear the tags captured by {@link DataPackContentsMixin}.
* This must happen after the resource reload is complete, to ensure that the tags remain available throughout the entire "apply" phase.
*/
@Inject(
method = "refresh",
at = @At("HEAD")
)
public void hookRefresh(CallbackInfo ci) {
private void hookRefresh(CallbackInfo ci) {
ResourceConditionsImpl.LOADED_TAGS.remove();
ResourceConditionsImpl.CURRENT_REGISTRIES.remove();
}
@Inject(
@ -54,7 +50,7 @@ public class DataPackContentsMixin {
at = @At("HEAD")
)
private static void hookReload(ResourceManager manager, CombinedDynamicRegistries<ServerDynamicRegistryType> combinedDynamicRegistries, FeatureSet enabledFeatures, CommandManager.RegistrationEnvironment environment, int functionPermissionLevel, Executor prepareExecutor, Executor applyExecutor, CallbackInfoReturnable<CompletableFuture<DataPackContents>> cir) {
ResourceConditionsImpl.CURRENT_FEATURES.set(enabledFeatures);
ResourceConditionsImpl.CURRENT_REGISTRIES.set(combinedDynamicRegistries.getCombinedRegistryManager());
System.out.println("Enabling " + FeatureFlags.FEATURE_MANAGER.toId(enabledFeatures));
ResourceConditionsImpl.currentFeatures = enabledFeatures;
}
}

View file

@ -21,16 +21,17 @@ import java.util.Map;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.resource.JsonDataLoader;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
import net.minecraft.util.profiler.Profiler;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
/**
@ -44,11 +45,10 @@ public class JsonDataLoaderMixin extends SinglePreparationResourceReloaderMixin
@Override
@SuppressWarnings("unchecked")
protected void fabric_applyResourceConditions(ResourceManager resourceManager, Profiler profiler, Object object) {
protected void fabric_applyResourceConditions(ResourceManager resourceManager, Profiler profiler, Object object, @Nullable RegistryWrapper.WrapperLookup registryLookup) {
profiler.push("Fabric resource conditions: %s".formatted(dataType));
Iterator<Map.Entry<Identifier, JsonElement>> it = ((Map<Identifier, JsonElement>) object).entrySet().iterator();
boolean debugLogEnabled = ResourceConditionsImpl.LOGGER.isDebugEnabled();
while (it.hasNext()) {
Map.Entry<Identifier, JsonElement> entry = it.next();
@ -57,17 +57,8 @@ public class JsonDataLoaderMixin extends SinglePreparationResourceReloaderMixin
if (resourceData.isJsonObject()) {
JsonObject obj = resourceData.getAsJsonObject();
if (obj.has(ResourceConditions.CONDITIONS_KEY)) {
boolean matched = ResourceConditions.objectMatchesConditions(obj);
if (!matched) {
it.remove();
}
if (debugLogEnabled) {
String verdict = matched ? "Allowed" : "Rejected";
ResourceConditionsImpl.LOGGER.debug("{} resource of type {} with id {}", verdict, dataType, entry.getKey());
}
if (!ResourceConditionsImpl.applyResourceConditions(obj, dataType, entry.getKey(), fabric_getRegistryLookup())) {
it.remove();
}
}
}

View file

@ -0,0 +1,37 @@
/*
* 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 org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.recipe.RecipeManager;
import net.minecraft.registry.RegistryWrapper;
@Mixin(RecipeManager.class)
public class RecipeManagerMixin extends SinglePreparationResourceReloaderMixin {
@Shadow
@Final
private RegistryWrapper.WrapperLookup registryLookup;
@Override
protected @Nullable RegistryWrapper.WrapperLookup fabric_getRegistryLookup() {
return this.registryLookup;
}
}

View file

@ -0,0 +1,76 @@
/*
* 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 java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import com.google.gson.JsonElement;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.loot.LootDataType;
import net.minecraft.registry.CombinedDynamicRegistries;
import net.minecraft.registry.DynamicRegistryManager;
import net.minecraft.registry.MutableRegistry;
import net.minecraft.registry.RegistryOps;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.ReloadableRegistries;
import net.minecraft.registry.ServerDynamicRegistryType;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
// Should apply before Loot API.
@Mixin(value = ReloadableRegistries.class, priority = 900)
public class ReloadableRegistriesMixin {
// The cross-thread nature of the stuff makes this necessary. It is technically possible to query the wrapper from
// the ops, but it requires more mixins.
// Key refers to value, but value does not refer to key, so WeakHashMap is fine.
@Unique
private static final WeakHashMap<RegistryOps<?>, RegistryWrapper.WrapperLookup> REGISTRY_LOOKUPS = new WeakHashMap<>();
@WrapOperation(method = "reload", at = @At(value = "NEW", target = "net/minecraft/registry/ReloadableRegistries$ReloadableWrapperLookup"))
private static ReloadableRegistries.ReloadableWrapperLookup storeWrapperLookup(DynamicRegistryManager registryManager, Operation<ReloadableRegistries.ReloadableWrapperLookup> original, @Share("wrapper") LocalRef<RegistryWrapper.WrapperLookup> share) {
ReloadableRegistries.ReloadableWrapperLookup lookup = original.call(registryManager);
share.set(lookup);
return lookup;
}
@Inject(method = "reload", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/registry/ReloadableRegistries$ReloadableWrapperLookup;getOps(Lcom/mojang/serialization/DynamicOps;)Lnet/minecraft/registry/RegistryOps;", shift = At.Shift.AFTER))
private static void storeWrapperLookup(CombinedDynamicRegistries<ServerDynamicRegistryType> dynamicRegistries, ResourceManager resourceManager, Executor prepareExecutor, CallbackInfoReturnable<CompletableFuture<CombinedDynamicRegistries<ServerDynamicRegistryType>>> cir, @Local RegistryOps ops, @Share("wrapper") LocalRef<RegistryWrapper.WrapperLookup> share) {
REGISTRY_LOOKUPS.put(ops, share.get());
}
@Inject(method = "method_58278", at = @At("HEAD"), cancellable = true)
private static void applyConditions(LootDataType lootDataType, RegistryOps ops, MutableRegistry mutableRegistry, Identifier id, JsonElement json, CallbackInfo ci) {
if (json.isJsonObject() && !ResourceConditionsImpl.applyResourceConditions(json.getAsJsonObject(), lootDataType.directory(), id, REGISTRY_LOOKUPS.get(ops))) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,37 @@
/*
* 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 org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.server.ServerAdvancementLoader;
@Mixin(ServerAdvancementLoader.class)
public class ServerAdvancementLoaderMixin extends SinglePreparationResourceReloaderMixin {
@Shadow
@Final
private RegistryWrapper.WrapperLookup registryLookup;
@Override
protected @Nullable RegistryWrapper.WrapperLookup fabric_getRegistryLookup() {
return this.registryLookup;
}
}

View file

@ -16,11 +16,13 @@
package net.fabricmc.fabric.mixin.resource.conditions;
import org.jetbrains.annotations.Nullable;
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.registry.RegistryWrapper;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.SinglePreparationResourceReloader;
import net.minecraft.util.profiler.Profiler;
@ -34,9 +36,14 @@ public class SinglePreparationResourceReloaderMixin {
// thenAcceptAsync in reload
@Inject(at = @At("HEAD"), method = "method_18790")
private void applyResourceConditions(ResourceManager resourceManager, Profiler profiler, Object object, CallbackInfo ci) {
fabric_applyResourceConditions(resourceManager, profiler, object);
fabric_applyResourceConditions(resourceManager, profiler, object, fabric_getRegistryLookup());
}
protected void fabric_applyResourceConditions(ResourceManager resourceManager, Profiler profiler, Object object) {
protected void fabric_applyResourceConditions(ResourceManager resourceManager, Profiler profiler, Object object, @Nullable RegistryWrapper.WrapperLookup registryLookup) {
}
@Nullable
protected RegistryWrapper.WrapperLookup fabric_getRegistryLookup() {
return null;
}
}

View file

@ -0,0 +1,3 @@
accessWidener v2 named
accessible class net/minecraft/registry/ReloadableRegistries$ReloadableWrapperLookup

View file

@ -6,6 +6,9 @@
"DataPackContentsMixin",
"DataProviderMixin",
"JsonDataLoaderMixin",
"RecipeManagerMixin",
"ReloadableRegistriesMixin",
"ServerAdvancementLoaderMixin",
"SinglePreparationResourceReloaderMixin",
"TagManagerLoaderMixin"
],

View file

@ -19,9 +19,15 @@
"fabricloader": ">=0.15.6"
},
"description": "Allows conditionally loading resources.",
"entrypoints": {
"main": [
"net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl"
]
},
"mixins": [
"fabric-resource-conditions-api-v1.mixins.json"
],
"accessWidener": "fabric-resource-conditions-api-v1.accesswidener",
"custom": {
"fabric-api:module-lifecycle": "stable"
}

View file

@ -16,7 +16,11 @@
package net.fabricmc.fabric.test.resource.conditions;
import net.minecraft.loot.LootTable;
import net.minecraft.recipe.RecipeManager;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.ReloadableRegistries;
import net.minecraft.test.GameTest;
import net.minecraft.test.TestContext;
import net.minecraft.util.Identifier;
@ -68,19 +72,18 @@ public class ConditionalResourcesTest {
context.complete();
}
/* TODO 1.20.5
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void conditionalPredicates(TestContext context) {
// Predicates are internally handled as a kind of loot data,
// hence the yarn name "loot condition".
LootManager manager = context.getWorld().getServer().getLootManager();
ReloadableRegistries.Lookup registries = context.getWorld().getServer().getReloadableRegistries();
if (manager.getElementOptional(LootDataType.PREDICATES, id("loaded")).isEmpty()) {
if (!registries.getRegistryManager().get(RegistryKeys.PREDICATE).containsId(id("loaded"))) {
throw new AssertionError("loaded predicate should have been loaded.");
}
if (manager.getElementOptional(LootDataType.PREDICATES, id("not_loaded")).isPresent()) {
if (registries.getRegistryManager().get(RegistryKeys.PREDICATE).containsId(id("not_loaded"))) {
throw new AssertionError("not_loaded predicate should not have been loaded.");
}
@ -89,16 +92,16 @@ public class ConditionalResourcesTest {
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void conditionalLootTables(TestContext context) {
LootManager manager = context.getWorld().getServer().getLootManager();
ReloadableRegistries.Lookup registries = context.getWorld().getServer().getReloadableRegistries();
if (manager.getElementOptional(LootDataType.LOOT_TABLES, id("blocks/loaded")).isEmpty()) {
if (registries.getLootTable(RegistryKey.of(RegistryKeys.LOOT_TABLE, id("blocks/loaded"))) == LootTable.EMPTY) {
throw new AssertionError("loaded loot table should have been loaded.");
}
if (manager.getElementOptional(LootDataType.LOOT_TABLES, id("blocks/not_loaded")).isPresent()) {
if (registries.getLootTable(RegistryKey.of(RegistryKeys.LOOT_TABLE, id("blocks/not_loaded"))) != LootTable.EMPTY) {
throw new AssertionError("not_loaded loot table should not have been loaded.");
}
context.complete();
}*/
}
}

View file

@ -0,0 +1,202 @@
/*
* 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.resource.conditions;
import java.util.stream.Collectors;
import com.mojang.serialization.JsonOps;
import net.minecraft.block.Block;
import net.minecraft.block.Blocks;
import net.minecraft.registry.DynamicRegistryManager;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.tag.BiomeTags;
import net.minecraft.registry.tag.BlockTags;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.resource.featuretoggle.FeatureFlag;
import net.minecraft.resource.featuretoggle.FeatureFlags;
import net.minecraft.test.GameTest;
import net.minecraft.test.TestContext;
import net.minecraft.util.Identifier;
import net.minecraft.world.biome.BiomeKeys;
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
public class DefaultResourceConditionsTest {
private static final String TESTMOD_ID = "fabric-resource-conditions-api-v1-testmod";
private static final String API_MOD_ID = "fabric-resource-conditions-api-v1";
private static final String UNKNOWN_MOD_ID = "fabric-tiny-potato-api-v1";
private static final RegistryKey<? extends Registry<Object>> UNKNOWN_REGISTRY_KEY = RegistryKey.ofRegistry(new Identifier(TESTMOD_ID, "unknown_registry"));
private static final Identifier UNKNOWN_ENTRY_ID = new Identifier(TESTMOD_ID, "tiny_potato");
private void expectCondition(TestContext context, String name, ResourceCondition condition, boolean expected) {
RegistryWrapper.WrapperLookup registryLookup = context.getWorld().getRegistryManager();
boolean actual = condition.test(registryLookup);
if (actual != expected) {
throw new AssertionError("Test \"%s\" for condition %s failed; expected %s, got %s".formatted(name, condition.getType().id(), expected, actual));
}
// Test serialization
ResourceCondition.CODEC.encodeStart(JsonOps.INSTANCE, condition).getOrThrow(message -> new AssertionError("Could not serialize \"%s\": %s".formatted(name, message)));
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void logics(TestContext context) {
ResourceCondition alwaysTrue = ResourceConditions.alwaysTrue();
ResourceCondition alwaysFalse = ResourceConditions.not(alwaysTrue);
ResourceCondition trueAndTrue = ResourceConditions.and(alwaysTrue, alwaysTrue);
ResourceCondition trueAndFalse = ResourceConditions.and(alwaysTrue, alwaysFalse);
ResourceCondition emptyAnd = ResourceConditions.and();
ResourceCondition trueOrFalse = ResourceConditions.or(alwaysTrue, alwaysFalse);
ResourceCondition falseOrFalse = ResourceConditions.or(alwaysFalse, alwaysFalse);
ResourceCondition emptyOr = ResourceConditions.or();
expectCondition(context, "always true", alwaysTrue, true);
expectCondition(context, "always false", alwaysFalse, false);
expectCondition(context, "true and true", trueAndTrue, true);
expectCondition(context, "true and false", trueAndFalse, false);
expectCondition(context, "vacuous truth", emptyAnd, true);
expectCondition(context, "true or false", trueOrFalse, true);
expectCondition(context, "false or false", falseOrFalse, false);
expectCondition(context, "empty OR is always false", emptyOr, false);
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void allModsLoaded(TestContext context) {
ResourceCondition testmod = ResourceConditions.allModsLoaded(TESTMOD_ID);
ResourceCondition testmodAndApi = ResourceConditions.allModsLoaded(TESTMOD_ID, API_MOD_ID);
ResourceCondition unknownMod = ResourceConditions.allModsLoaded(UNKNOWN_MOD_ID);
ResourceCondition unknownAndTestmod = ResourceConditions.allModsLoaded(UNKNOWN_MOD_ID, TESTMOD_ID);
ResourceCondition noMod = ResourceConditions.allModsLoaded();
expectCondition(context, "one loaded mod", testmod, true);
expectCondition(context, "two loaded mods", testmodAndApi, true);
expectCondition(context, "one unloaded mod", unknownMod, false);
expectCondition(context, "both loaded and unloaded mods", unknownAndTestmod, false);
expectCondition(context, "no mod", noMod, true);
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void anyModsLoaded(TestContext context) {
ResourceCondition testmod = ResourceConditions.anyModsLoaded(TESTMOD_ID);
ResourceCondition testmodAndApi = ResourceConditions.anyModsLoaded(TESTMOD_ID, API_MOD_ID);
ResourceCondition unknownMod = ResourceConditions.anyModsLoaded(UNKNOWN_MOD_ID);
ResourceCondition unknownAndTestmod = ResourceConditions.anyModsLoaded(UNKNOWN_MOD_ID, TESTMOD_ID);
ResourceCondition noMod = ResourceConditions.anyModsLoaded();
expectCondition(context, "one loaded mod", testmod, true);
expectCondition(context, "two loaded mods", testmodAndApi, true);
expectCondition(context, "one unloaded mod", unknownMod, false);
expectCondition(context, "both loaded and unloaded mods", unknownAndTestmod, true);
expectCondition(context, "no mod", noMod, false);
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void featuresEnabled(TestContext context) {
ResourceCondition vanilla = ResourceConditions.featuresEnabled(FeatureFlags.VANILLA);
// Reminder: GameTest enables all features by default
ResourceCondition vanillaAndBundle = ResourceConditions.featuresEnabled(FeatureFlags.VANILLA, FeatureFlags.BUNDLE);
Identifier unknownId = new Identifier(TESTMOD_ID, "unknown_feature_to_test_condition");
ResourceCondition unknown = ResourceConditions.featuresEnabled(unknownId);
// Passing an array to avoid type ambiguity
ResourceCondition empty = ResourceConditions.featuresEnabled(new FeatureFlag[]{});
expectCondition(context, "vanilla only", vanilla, true);
expectCondition(context, "vanilla and bundle", vanillaAndBundle, true);
expectCondition(context, "unknown feature ID", unknown, false);
expectCondition(context, "no feature", empty, true);
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void registryContains(TestContext context) {
// Static registry
RegistryKey<Block> dirtKey = Registries.BLOCK.getKey(Blocks.DIRT).orElseThrow();
ResourceCondition dirt = ResourceConditions.registryContains(dirtKey);
ResourceCondition dirtAndUnknownBlock = ResourceConditions.registryContains(dirtKey, RegistryKey.of(RegistryKeys.BLOCK, UNKNOWN_ENTRY_ID));
ResourceCondition emptyBlock = ResourceConditions.registryContains(RegistryKeys.BLOCK, new Identifier[]{});
ResourceCondition unknownRegistry = ResourceConditions.registryContains(UNKNOWN_REGISTRY_KEY, UNKNOWN_ENTRY_ID);
ResourceCondition emptyUnknown = ResourceConditions.registryContains(UNKNOWN_REGISTRY_KEY, new Identifier[]{});
expectCondition(context, "dirt", dirt, true);
expectCondition(context, "dirt and unknown block", dirtAndUnknownBlock, false);
expectCondition(context, "block registry, empty check", emptyBlock, true);
expectCondition(context, "unknown registry, non-empty", unknownRegistry, false);
expectCondition(context, "unknown registry, empty", emptyUnknown, true);
// Dynamic registry (in vitro; separate testmod needs to determine if this actually functions while loading)
ResourceCondition plains = ResourceConditions.registryContains(BiomeKeys.PLAINS);
ResourceCondition unknownBiome = ResourceConditions.registryContains(RegistryKey.of(RegistryKeys.BIOME, UNKNOWN_ENTRY_ID));
ResourceCondition emptyDynamic = ResourceConditions.registryContains(RegistryKeys.BIOME, new Identifier[]{});
expectCondition(context, "plains", plains, true);
expectCondition(context, "unknown biome", unknownBiome, false);
expectCondition(context, "biome registry, empty check", emptyDynamic, true);
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void tagsPopulated(TestContext context) {
// We need to set the tags ourselves as it is cleared outside the resource loading context.
ResourceConditionsImpl.LOADED_TAGS.set(
context.getWorld().getRegistryManager().streamAllRegistries().collect(Collectors.toMap(
DynamicRegistryManager.Entry::key,
e -> e.value().streamTags().map(TagKey::id).collect(Collectors.toUnmodifiableSet())
))
);
// Static registry
ResourceCondition dirt = ResourceConditions.tagsPopulated(RegistryKeys.BLOCK, BlockTags.DIRT);
ResourceCondition dirtAndUnknownBlock = ResourceConditions.tagsPopulated(RegistryKeys.BLOCK, BlockTags.DIRT, TagKey.of(RegistryKeys.BLOCK, UNKNOWN_ENTRY_ID));
ResourceCondition emptyBlock = ResourceConditions.tagsPopulated(RegistryKeys.BLOCK);
ResourceCondition unknownRegistry = ResourceConditions.tagsPopulated(UNKNOWN_REGISTRY_KEY, TagKey.of(UNKNOWN_REGISTRY_KEY, UNKNOWN_ENTRY_ID));
ResourceCondition emptyUnknown = ResourceConditions.tagsPopulated(UNKNOWN_REGISTRY_KEY);
expectCondition(context, "dirt tag", dirt, true);
expectCondition(context, "dirt tag and unknown tag", dirtAndUnknownBlock, false);
expectCondition(context, "block registry, empty tag checks", emptyBlock, true);
expectCondition(context, "unknown registry, non-empty tag checks", unknownRegistry, false);
expectCondition(context, "unknown registry, empty tag checks", emptyUnknown, true);
// Dynamic registry (in vitro; separate testmod needs to determine if this actually functions while loading)
ResourceCondition forest = ResourceConditions.tagsPopulated(RegistryKeys.BIOME, BiomeTags.IS_FOREST);
ResourceCondition unknownBiome = ResourceConditions.tagsPopulated(RegistryKeys.BIOME, TagKey.of(RegistryKeys.BIOME, UNKNOWN_ENTRY_ID));
ResourceCondition emptyDynamic = ResourceConditions.tagsPopulated(RegistryKeys.BIOME);
expectCondition(context, "forest tag", forest, true);
expectCondition(context, "unknown biome tag", unknownBiome, false);
expectCondition(context, "biome registry, empty tag check", emptyDynamic, true);
context.complete();
}
}

View file

@ -10,7 +10,8 @@
},
"fabric:load_conditions": [
{
"condition": "fabric:item_tags_populated",
"condition": "fabric:tags_populated",
"registry": "minecraft:item",
"values": [
"fabric-resource-conditions-api-v1-testmod:test_condition"
]

View file

@ -10,7 +10,8 @@
},
"entrypoints": {
"fabric-gametest": [
"net.fabricmc.fabric.test.resource.conditions.ConditionalResourcesTest"
"net.fabricmc.fabric.test.resource.conditions.ConditionalResourcesTest",
"net.fabricmc.fabric.test.resource.conditions.DefaultResourceConditionsTest"
]
}
}