Fabric Resource Conditions ()

* First completed draft

* Update PR, should be close to ready now

* Add fluid/item_tags_populated conditions

* Log processed resource with trace log level

* Use debug instead of trace log level

* Apply suggestions from code review

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

* Code review suggestions

* writeAdditional -> writeParameters

* Apply suggestions from code review
Co-authored-by: Juuxel <6596629+Juuxel@users.noreply.github.com>

* CONDITION_ID -> CONDITION_ID_KEY
This commit is contained in:
Technici4n 2022-01-14 16:08:18 +01:00 committed by modmuss50
parent eb6d303987
commit 12540453db
21 changed files with 871 additions and 8 deletions
fabric-data-generation-api-v1
build.gradle
src
main/java/net/fabricmc/fabric
testmod/java/net/fabricmc/fabric/test/datagen
fabric-resource-conditions-api-v1
build.gradle
src
main
java/net/fabricmc/fabric
resources
testmod
java/net/fabricmc/fabric/test/resource/conditions
resources
data/fabric-resource-conditions-api-v1-testmod/recipes
fabric.mod.json
gradle.propertiessettings.gradle

View file

@ -4,7 +4,8 @@ version = getSubprojectVersion(project)
moduleDependencies(project, [
'fabric-api-base',
'fabric-registry-sync-v0',
'fabric-networking-api-v1'
'fabric-networking-api-v1',
'fabric-resource-conditions-api-v1'
])
dependencies {

View file

@ -21,9 +21,11 @@ import java.nio.file.Path;
import java.util.Set;
import java.util.function.Consumer;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import net.minecraft.advancement.Advancement;
import net.minecraft.data.DataCache;
@ -31,6 +33,8 @@ import net.minecraft.data.DataProvider;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
/**
* Extend this class and implement {@link FabricAdvancementsProvider#generateAdvancement}.
@ -53,6 +57,17 @@ public abstract class FabricAdvancementsProvider implements DataProvider {
*/
public abstract void generateAdvancement(Consumer<Advancement> consumer);
/**
* Return a new exporter that applies the specified conditions to any advancement it receives.
*/
protected Consumer<Advancement> withConditions(Consumer<Advancement> exporter, ConditionJsonProvider... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must add at least one condition.");
return advancement -> {
FabricDataGenHelper.addConditions(advancement, conditions);
exporter.accept(advancement);
};
}
@Override
public void run(DataCache cache) throws IOException {
final Set<Identifier> identifiers = Sets.newHashSet();
@ -65,7 +80,10 @@ public abstract class FabricAdvancementsProvider implements DataProvider {
throw new IllegalStateException("Duplicate advancement " + advancement.getId());
}
DataProvider.writeToPath(GSON, cache, advancement.createTask().toJson(), getOutputPath(advancement));
JsonObject advancementJson = advancement.createTask().toJson();
ConditionJsonProvider.write(advancementJson, FabricDataGenHelper.consumeConditions(advancement));
DataProvider.writeToPath(GSON, cache, advancementJson, getOutputPath(advancement));
}
}

View file

@ -23,9 +23,11 @@ import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.data.DataCache;
@ -36,6 +38,8 @@ import net.minecraft.loot.context.LootContextType;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
/**
* A base interface for Loot table providers. You should not implement this class directly.
@ -52,19 +56,37 @@ public interface FabricLootTableProvider extends Consumer<BiConsumer<Identifier,
FabricDataGenerator getFabricDataGenerator();
/**
* Return a new exporter that applies the specified conditions to any loot table it receives.
*/
default BiConsumer<Identifier, LootTable.Builder> withConditions(BiConsumer<Identifier, LootTable.Builder> exporter, ConditionJsonProvider... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must add at least one condition.");
return (id, table) -> {
FabricDataGenHelper.addConditions(table, conditions);
exporter.accept(id, table);
};
}
@ApiStatus.Internal
@Override
default void run(DataCache cache) throws IOException {
HashMap<Identifier, LootTable> builders = Maps.newHashMap();
HashMap<Identifier, ConditionJsonProvider[]> conditionMap = new HashMap<>();
accept((identifier, builder) -> {
ConditionJsonProvider[] conditions = FabricDataGenHelper.consumeConditions(builder);
conditionMap.put(identifier, conditions);
if (builders.put(identifier, builder.type(getLootContextType()).build()) != null) {
throw new IllegalStateException("Duplicate loot table " + identifier);
}
});
for (Map.Entry<Identifier, LootTable> entry : builders.entrySet()) {
DataProvider.writeToPath(GSON, cache, LootManager.toJson(entry.getValue()), getOutputPath(entry.getKey()));
JsonObject tableJson = (JsonObject) LootManager.toJson(entry.getValue());
ConditionJsonProvider.write(tableJson, conditionMap.remove(entry.getKey()));
DataProvider.writeToPath(GSON, cache, tableJson, getOutputPath(entry.getKey()));
}
}

View file

@ -20,6 +20,7 @@ import java.nio.file.Path;
import java.util.Set;
import java.util.function.Consumer;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.gson.JsonObject;
@ -31,6 +32,8 @@ import net.minecraft.data.server.recipe.ShapelessRecipeJsonFactory;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper;
/**
* Extend this class and implement {@link FabricRecipesProvider#generateRecipes}.
@ -50,6 +53,17 @@ public abstract class FabricRecipesProvider extends RecipesProvider {
*/
protected abstract void generateRecipes(Consumer<RecipeJsonProvider> exporter);
/**
* Return a new exporter that applies the specified conditions to any recipe json provider it receives.
*/
protected Consumer<RecipeJsonProvider> withConditions(Consumer<RecipeJsonProvider> exporter, ConditionJsonProvider... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must add at least one condition.");
return json -> {
FabricDataGenHelper.addConditions(json, conditions);
exporter.accept(json);
};
}
@Override
public void run(DataCache cache) {
Path path = this.root.getOutput();
@ -61,11 +75,16 @@ public abstract class FabricRecipesProvider extends RecipesProvider {
throw new IllegalStateException("Duplicate recipe " + identifier);
}
saveRecipe(cache, provider.toJson(), path.resolve("data/" + identifier.getNamespace() + "/recipes/" + identifier.getPath() + ".json"));
JsonObject jsonObject = provider.toAdvancementJson();
JsonObject recipeJson = provider.toJson();
ConditionJsonProvider[] conditions = FabricDataGenHelper.consumeConditions(provider);
ConditionJsonProvider.write(recipeJson, conditions);
if (jsonObject != null) {
saveRecipeAdvancement(cache, jsonObject, path.resolve("data/" + identifier.getNamespace() + "/advancements/" + provider.getAdvancementId().getPath() + ".json"));
saveRecipe(cache, recipeJson, path.resolve("data/" + identifier.getNamespace() + "/recipes/" + identifier.getPath() + ".json"));
JsonObject advancementJson = provider.toAdvancementJson();
if (advancementJson != null) {
ConditionJsonProvider.write(advancementJson, conditions);
saveRecipeAdvancement(cache, advancementJson, path.resolve("data/" + identifier.getNamespace() + "/advancements/" + provider.getAdvancementId().getPath() + ".json"));
}
});
}

View file

@ -19,10 +19,13 @@ package net.fabricmc.fabric.impl.datagen;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.mojang.serialization.Lifecycle;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.ApiStatus;
@ -37,6 +40,7 @@ import net.minecraft.util.registry.SimpleRegistry;
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider.DynamicRegistryTagProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.entrypoint.EntrypointContainer;
@ -117,4 +121,18 @@ public final class FabricDataGenHelper {
public static <T> Registry<T> getFakeDynamicRegistry() {
return FAKE_DYNAMIC_REGISTRY;
}
/**
* Used to keep track of conditions associated to generated objects.
*/
private static final Map<Object, ConditionJsonProvider[]> CONDITIONS_MAP = new IdentityHashMap<>();
public static void addConditions(Object object, ConditionJsonProvider[] conditions) {
CONDITIONS_MAP.merge(object, conditions, ArrayUtils::addAll);
}
@Nullable
public static ConditionJsonProvider[] consumeConditions(Object object) {
return CONDITIONS_MAP.remove(object);
}
}

View file

@ -29,6 +29,8 @@ import net.minecraft.advancement.criterion.OnKilledCriterion;
import net.minecraft.data.client.ItemModelGenerator;
import net.minecraft.data.client.model.BlockStateModelGenerator;
import net.minecraft.data.server.recipe.RecipeJsonProvider;
import net.minecraft.data.server.recipe.ShapelessRecipeJsonFactory;
import net.minecraft.item.Items;
import net.minecraft.loot.LootPool;
import net.minecraft.loot.LootTable;
import net.minecraft.loot.LootTables;
@ -53,12 +55,18 @@ import net.fabricmc.fabric.api.datagen.v1.provider.FabricBlockStateDefinitionPro
import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipesProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider;
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.DefaultResourceConditions;
import net.fabricmc.fabric.api.tag.TagFactory;
public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
private static final ConditionJsonProvider NEVER_LOADED = DefaultResourceConditions.allModsLoaded("a");
private static final ConditionJsonProvider ALWAYS_LOADED = DefaultResourceConditions.not(NEVER_LOADED);
@Override
public void onInitializeDataGenerator(FabricDataGenerator dataGenerator) {
dataGenerator.addProvider(TestRecipeProvider::new);
dataGenerator.addProvider(TestConditionalRecipeProvider::new);
dataGenerator.addProvider(TestBlockStateDefinitionProvider::new);
dataGenerator.addProvider(TestAdvancementsProvider::new);
dataGenerator.addProvider(TestBlockLootTablesProvider::new);
@ -102,6 +110,18 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
}
}
private static class TestConditionalRecipeProvider extends FabricRecipesProvider {
private TestConditionalRecipeProvider(FabricDataGenerator dataGenerator) {
super(dataGenerator);
}
@Override
protected void generateRecipes(Consumer<RecipeJsonProvider> exporter) {
ShapelessRecipeJsonFactory.create(Items.GOLD_INGOT).input(Items.DIRT).criterion("has_dirt", conditionsFromItem(Items.DIRT)).offerTo(withConditions(exporter, NEVER_LOADED));
ShapelessRecipeJsonFactory.create(Items.DIAMOND).input(Items.STICK).criterion("has_stick", conditionsFromItem(Items.STICK)).offerTo(withConditions(exporter, ALWAYS_LOADED));
}
}
private static class TestBlockStateDefinitionProvider extends FabricBlockStateDefinitionProvider {
private TestBlockStateDefinitionProvider(FabricDataGenerator generator) {
super(generator);
@ -180,6 +200,16 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
false, false, false)
.criterion("killed_something", OnKilledCriterion.Conditions.createPlayerKilledEntity())
.build(consumer, MOD_ID + ":test/root");
Advancement rootNotLoaded = Advancement.Task.create()
.display(
SIMPLE_BLOCK,
new TranslatableText("advancements.test.root_not_loaded.title"),
new TranslatableText("advancements.test.root_not_loaded.description"),
new Identifier("textures/gui/advancements/backgrounds/end.png"),
AdvancementFrame.TASK,
false, false, false)
.criterion("killed_something", OnKilledCriterion.Conditions.createPlayerKilledEntity())
.build(withConditions(consumer, NEVER_LOADED), MOD_ID + ":test/root_not_loaded");
}
}
@ -202,7 +232,7 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
@Override
public void accept(BiConsumer<Identifier, LootTable.Builder> consumer) {
consumer.accept(
withConditions(consumer, ALWAYS_LOADED).accept(
LootTables.PIGLIN_BARTERING_GAMEPLAY,
LootTable.builder().pool(
LootPool.builder().rolls(ConstantLootNumberProvider.create(1.0F)).with(ItemEntry.builder(SIMPLE_BLOCK))

View file

@ -0,0 +1,6 @@
archivesBaseName = "fabric-resource-conditions-api-v1"
version = getSubprojectVersion(project)
dependencies {
testmodImplementation project(path: ':fabric-gametest-api-v1', configuration: 'namedElements')
}

View file

@ -0,0 +1,71 @@
/*
* 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 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) {
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

@ -0,0 +1,139 @@
/*
* 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 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.tag.BlockTags;
import net.minecraft.tag.FluidTags;
import net.minecraft.tag.ItemTags;
import net.minecraft.tag.Tag;
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");
/**
* Create a NOT condition: returns true if its child condition is false, and false if its child is true.
*/
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;
}
};
}
/**
* Create a condition that returns true if all of its child conditions are true.
*/
public static ConditionJsonProvider and(ConditionJsonProvider... values) {
return ResourceConditionsImpl.array(AND, values);
}
/**
* Create a condition that returns true if at least one of its child conditions is true.
*/
public static ConditionJsonProvider or(ConditionJsonProvider... values) {
return ResourceConditionsImpl.array(OR, values);
}
/**
* Create a condition that returns true if all the passed mod ids correspond to a loaded mod.
*/
public static ConditionJsonProvider allModsLoaded(String... modIds) {
return ResourceConditionsImpl.mods(ALL_MODS_LOADED, modIds);
}
/**
* Create a condition that returns true if at least one of the passed mod ids corresponds to a loaded mod.
*/
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.
*/
public static ConditionJsonProvider blockTagsPopulated(Tag.Identified<Block>... tags) {
return ResourceConditionsImpl.tagsPopulated(BLOCK_TAGS_POPULATED, tags);
}
/**
* Create a condition that returns true if each of the passed fluid tags exists and has at least one element.
*/
public static ConditionJsonProvider fluidTagsPopulated(Tag.Identified<Fluid>... tags) {
return ResourceConditionsImpl.tagsPopulated(FLUID_TAGS_POPULATED, tags);
}
/**
* Create a condition that returns true if each of the passed item tags exists and has at least one element.
*/
public static ConditionJsonProvider itemTagsPopulated(Tag.Identified<Item>... tags) {
return ResourceConditionsImpl.tagsPopulated(ITEM_TAGS_POPULATED, tags);
}
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, BlockTags.getTagGroup()));
ResourceConditions.register(FLUID_TAGS_POPULATED, object -> ResourceConditionsImpl.tagsPopulatedMatch(object, FluidTags.getTagGroup()));
ResourceConditions.register(ITEM_TAGS_POPULATED, object -> ResourceConditionsImpl.tagsPopulatedMatch(object, ItemTags.getTagGroup()));
}
private DefaultResourceConditions() {
}
}

View file

@ -0,0 +1,176 @@
/*
* 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.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.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.impl.resource.conditions.ResourceConditionsImpl;
/**
* 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.</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>
*/
public final class ResourceConditions {
private static final Map<Identifier, Predicate<JsonObject>> REGISTERED_CONDITIONS = new ConcurrentHashMap<>();
/**
* The key ({@value}) Fabric uses to identify resource conditions in a JSON object.
*/
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>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();
}
}

View file

@ -0,0 +1,144 @@
/*
* 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.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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.tag.Tag;
import net.minecraft.tag.TagGroup;
import net.minecraft.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.loader.api.FabricLoader;
@ApiStatus.Internal
public class ResourceConditionsImpl {
public static final Logger LOGGER = LogManager.getLogger("Fabric Resource Conditions");
// 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);
}
};
}
public static ConditionJsonProvider mods(Identifier id, String... modIds) {
Preconditions.checkArgument(modIds.length > 0, "Must register at least one mod id.");
return new ConditionJsonProvider() {
@Override
public Identifier getConditionId() {
return id;
}
@Override
public void writeParameters(JsonObject object) {
JsonArray array = new JsonArray();
for (String modId : modIds) {
array.add(modId);
}
object.add("values", array);
}
};
}
public static <T> ConditionJsonProvider tagsPopulated(Identifier id, Tag.Identified<T>... tags) {
Preconditions.checkArgument(tags.length > 0, "Must register at least one tag.");
return new ConditionJsonProvider() {
@Override
public Identifier getConditionId() {
return id;
}
@Override
public void writeParameters(JsonObject object) {
JsonArray array = new JsonArray();
for (Tag.Identified<T> tag : tags) {
array.add(tag.getId().toString());
}
object.add("values", array);
}
};
}
// Condition implementations
public static boolean modsLoadedMatch(JsonObject object, boolean and) {
JsonArray array = JsonHelper.getArray(object, "values");
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 <T> boolean tagsPopulatedMatch(JsonObject object, TagGroup<T> tagGroup) {
JsonArray array = JsonHelper.getArray(object, "values");
for (JsonElement element : array) {
if (element.isJsonPrimitive()) {
Identifier id = new Identifier(element.getAsString());
Tag<T> tag = tagGroup.getTag(id);
if (tag == null || tag.values().size() == 0) {
return false;
}
} else {
throw new JsonParseException("Invalid tag id entry: " + element);
}
}
return true;
}
}

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.Iterator;
import java.util.Map;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
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;
@Mixin(JsonDataLoader.class)
public class JsonDataLoaderMixin {
@Shadow
@Final
private String dataType;
@Inject(at = @At("RETURN"), method = "prepare")
public void applyResourceConditions(ResourceManager resourceManager, Profiler profiler, CallbackInfoReturnable<Map<Identifier, JsonElement>> cir) {
profiler.push("Fabric resource conditions: %s".formatted(dataType));
Iterator<Map.Entry<Identifier, JsonElement>> it = cir.getReturnValue().entrySet().iterator();
boolean debugLogEnabled = ResourceConditionsImpl.LOGGER.isDebugEnabled();
while (it.hasNext()) {
Map.Entry<Identifier, JsonElement> entry = it.next();
JsonElement resourceData = entry.getValue();
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());
}
}
}
}
profiler.pop();
}
}

View file

@ -0,0 +1,8 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.resource.conditions",
"compatibilityLevel": "JAVA_16",
"mixins": [
"JsonDataLoaderMixin"
]
}

View file

@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "fabric-resource-conditions-api-v1",
"name": "Fabric Resource Conditions API (v1)",
"version": "${version}",
"environment": "*",
"license": "Apache-2.0",
"icon": "assets/fabric-resource-conditions-api-v1/icon.png",
"contact": {
"homepage": "https://fabricmc.net",
"irc": "irc://irc.esper.net:6667/fabric",
"issues": "https://github.com/FabricMC/fabric/issues",
"sources": "https://github.com/FabricMC/fabric"
},
"authors": [
"FabricMC"
],
"depends": {
"fabricloader": ">=0.9.2"
},
"description": "Allows conditionally loading resources.",
"mixins": [
"fabric-resource-conditions-api-v1.mixins.json"
],
"custom": {
"fabric-api:module-lifecycle": "experimental"
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 net.minecraft.recipe.RecipeManager;
import net.minecraft.test.GameTest;
import net.minecraft.test.TestContext;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
public class ConditionalResourcesTest {
private static final String MOD_ID = "fabric-resource-conditions-api-v1-testmod";
private static Identifier id(String path) {
return new Identifier(MOD_ID, path);
}
@GameTest(structureName = FabricGameTest.EMPTY_STRUCTURE)
public void conditionalRecipes(TestContext context) {
RecipeManager manager = context.getWorld().getRecipeManager();
if (manager.get(id("not_loaded")).isPresent()) {
throw new AssertionError("not_loaded recipe should not have been loaded.");
}
if (manager.get(id("loaded")).isEmpty()) {
throw new AssertionError("loaded recipe should have been loaded.");
}
long loadedRecipes = manager.values().stream().filter(r -> r.getId().getNamespace().equals(MOD_ID)).count();
if (loadedRecipes != 1) throw new AssertionError("Unexpected loaded recipe count: " + loadedRecipes);
context.complete();
}
}

View file

@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shapeless",
"ingredients": [
{
"item": "minecraft:stick"
}
],
"result": {
"item": "minecraft:diamond"
},
"fabric:load_conditions": [
{
"condition": "fabric:all_mods_loaded",
"values": [
"fabric-resource-conditions-api-v1"
]
}
]
}

View file

@ -0,0 +1,20 @@
{
"__comment": "this is going to be skipped before any check, so /shrug.",
"fabric:load_conditions": [
{
"condition": "fabric:not",
"value": {
"condition": "fabric:all_mods_loaded",
"values": [
"fabric-resource-conditions-api-v1"
]
}
},
{
"condition": "fabric:all_mods_loaded",
"values": [
"fabric-resource-conditions-api-v1"
]
}
]
}

View file

@ -0,0 +1,16 @@
{
"schemaVersion": 1,
"id": "fabric-resource-conditions-api-v1-testmod",
"name": "Fabric Resource Conditions API (v1) Test Mod",
"version": "1.0.0",
"environment": "*",
"license": "Apache-2.0",
"depends": {
"fabric-api-lookup-api-v1": "*"
},
"entrypoints": {
"fabric-gametest": [
"net.fabricmc.fabric.test.resource.conditions.ConditionalResourcesTest"
]
}
}

View file

@ -46,6 +46,7 @@ fabric-rendering-data-attachment-v1-version=0.3.4
fabric-rendering-fluids-v1-version=0.1.18
fabric-rendering-v0-version=1.1.9
fabric-rendering-v1-version=1.10.3
fabric-resource-conditions-api-v1-version=1.0.0
fabric-resource-loader-v0-version=0.4.11
fabric-screen-api-v1-version=1.0.7
fabric-screen-handler-api-v1-version=1.1.11

View file

@ -50,6 +50,7 @@ include 'fabric-rendering-v0'
include 'fabric-rendering-v1'
include 'fabric-rendering-data-attachment-v1'
include 'fabric-rendering-fluids-v1'
include 'fabric-resource-conditions-api-v1'
include 'fabric-resource-loader-v0'
include 'fabric-screen-api-v1'
include 'fabric-screen-handler-api-v1'