Revive ConditionalRecipe and ConditionalAdvancement.

Also directly add conditional support to all vanilla recipe data generators.
This commit is contained in:
LexManos committed 2023-10-06 21:01:03 -07:00
1 parent 90dafb0df0
commit 587b684035
49 files changed
+1826 -512

No files matched your search

@@ -22,12 +22,16 @@ import java.util.stream.Stream;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.Decoder;
import com.mojang.serialization.Dynamic;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
@@ -51,6 +55,7 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagKey;
import net.minecraft.util.CrudeIncrementalIntIdentityHashBiMap;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.Mth;
import net.minecraft.util.datafix.fixes.StructuresBecomeConfiguredFix;
import net.minecraft.world.Difficulty;
@@ -112,6 +117,7 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSpecialEffects;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraftforge.common.crafting.conditions.ICondition;
import net.minecraftforge.common.util.BlockSnapshot;
import net.minecraftforge.common.util.BrainBuilder;
import net.minecraftforge.common.util.Lazy;
@@ -185,6 +191,7 @@ import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.damagesource.DamageSource;
@@ -1268,4 +1275,42 @@ public class ForgeHooks {
var pkt = (Packet<ClientGamePacketListener>)NetworkDirection.PLAY_TO_CLIENT.buildPacket(data, play.getName()).getThis();
return pkt;
}
@ApiStatus.Internal
public static boolean readAndTestCondition(ICondition.IContext context, JsonObject json) {
if (!json.has(ICondition.DEFAULT_FIELD))
return true;
var condition = Util.getOrThrow(ICondition.SAFE_CODEC.parse(JsonOps.INSTANCE, json.getAsJsonObject(ICondition.DEFAULT_FIELD)), JsonParseException::new);
return condition.test(context);
}
@ApiStatus.Internal
public static void writeCondition(ICondition condition, JsonObject out) {
if (condition == null)
return;
var data = ICondition.CODEC.encode(condition, JsonOps.INSTANCE, JsonOps.INSTANCE.empty()).getOrThrow(false, JsonSyntaxException::new);
out.add(ICondition.DEFAULT_FIELD, data);
}
@Nullable
@ApiStatus.Internal
public static JsonObject readConditionalAdvancement(ICondition.IContext context, JsonObject json) {
var entries = GsonHelper.getAsJsonArray(json, "advancements", null);
if (entries == null)
return readAndTestCondition(context, json) ? json : null;
int idx = 0;
for (var ele : entries) {
if (!ele.isJsonObject())
throw new JsonSyntaxException("Invalid advancement entry at index " + idx + " Must be JsonObject");
if (readAndTestCondition(context, ele.getAsJsonObject()))
return GsonHelper.getAsJsonObject(ele.getAsJsonObject(), "advancement");
idx++;
}
return null;
}
}
@@ -28,6 +28,7 @@ import net.minecraft.world.entity.ai.attributes.RangedAttribute;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.biome.Biome;
@@ -93,6 +94,7 @@ import org.apache.logging.log4j.Logger;
import net.minecraft.data.DataGenerator;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.common.crafting.ConditionalRecipe;
import net.minecraftforge.common.crafting.conditions.AndCondition;
import net.minecraftforge.common.crafting.conditions.FalseCondition;
import net.minecraftforge.common.crafting.conditions.ICondition;
@@ -390,6 +392,11 @@ public class ForgeMod {
CONDITION_SERIALIZERS.register("tag_empty", () -> TagEmptyCondition.CODEC);
}
private static final DeferredRegister<RecipeSerializer<?>> RECIPE_SERIALIZERS = DeferredRegister.create(ForgeRegistries.Keys.RECIPE_SERIALIZERS, "forge");
static {
RECIPE_SERIALIZERS.register("conditional", () -> ConditionalRecipe.SERIALZIER);
}
private static boolean enableMilkFluid = false;
public static final RegistryObject<SoundEvent> BUCKET_EMPTY_MILK = RegistryObject.create(new ResourceLocation("item.bucket.empty_milk"), ForgeRegistries.SOUND_EVENTS);
@@ -447,6 +454,7 @@ public class ForgeMod {
VANILLA_FLUID_TYPES.register(modEventBus);
LOOT_CONDITION_TYPES.register(modEventBus);
CONDITION_SERIALIZERS.register(modEventBus);
RECIPE_SERIALIZERS.register(modEventBus);
MinecraftForge.EVENT_BUS.addListener(this::serverStopping);
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, ForgeConfig.clientSpec);
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ForgeConfig.serverSpec);
@@ -4,123 +4,113 @@
*/
package net.minecraftforge.common.crafting;
/*
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import net.minecraft.advancements.Advancement;
import net.minecraft.advancements.AdvancementHolder;
import net.minecraft.data.recipes.FinishedRecipe;
import net.minecraft.util.GsonHelper;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.common.crafting.conditions.ICondition;
import org.jetbrains.annotations.Nullable;
public class ConditionalAdvancement
{
public static Builder builder()
{
/**
* A `ConditionalAdvancement` is a single advancment file that contains multiple advancements, each having a condition.
* When loaded it will return the first advancement that the conditions pass.
*
* This allows for multiple variants of an advancement to share the same name in the registry. Which allows dependents
* to reference it without having to care about the conditions themselves.
*
* This is most likely useful when you have variants of a recipe based on what mods/resources are installed but want
* to maintain the same 'entry' in the advancement book.
*/
public class ConditionalAdvancement {
public static Builder builder() {
return new Builder();
}
/**
* Processes the conditional advancement during loading.
* @param json The incoming json from the advancement file.
* @return The advancement that passed the conditions, or null if none did.
* /
@Nullable
public static JsonObject processConditional(JsonObject json, ICondition.IContext context) {
JsonArray entries = GsonHelper.getAsJsonArray(json, "advancements", null);
if (entries == null)
{
return CraftingHelper.processConditions(json, "conditions", context) ? json : null;
public static class Builder {
private static final ResourceLocation DOESNT_MATTER = new ResourceLocation("doesnt", "matter");
private List<Pair> advancements = new ArrayList<>();
private ICondition condition;
public Builder condition(ICondition value) {
this.condition = value;
return this;
}
int idx = 0;
for (JsonElement ele : entries)
{
if (!ele.isJsonObject())
throw new JsonSyntaxException("Invalid advancement entry at index " + idx + " Must be JsonObject");
if (CraftingHelper.processConditions(GsonHelper.getAsJsonArray(ele.getAsJsonObject(), "conditions"), context))
return GsonHelper.getAsJsonObject(ele.getAsJsonObject(), "advancement");
idx++;
public Builder advancement(Consumer<Consumer<Advancement.Builder>> callable) {
callable.accept(this::advancement);
return this;
}
public Builder advancement(Advancement.Builder builder) {
return advancement(builder.build(DOESNT_MATTER).value());
}
public Builder advancement(FinishedRecipe fromRecipe) {
return advancement(fromRecipe.advancement());
}
public Builder advancement(AdvancementHolder holder) {
return advancement(holder.value());
}
private Builder advancement(Advancement value) {
if (condition == null)
throw new IllegalStateException("Can not add a advancement with no conditions.");
if (value == null)
throw new IllegalStateException("Can not add a null advancement");
this.advancements.add(new Pair(this.condition, value));
this.condition = null;
return this;
}
public IResult build() {
return new Result(List.copyOf(advancements));
}
public JsonObject write() {
var json = new JsonObject();
var array = new JsonArray();
json.add("advancements", array);
for (var pair : advancements) {
var holder = pair.adv().serializeToJson();
if (holder.has(ICondition.DEFAULT_FIELD))
throw new IllegalStateException("Recipe already serialized conditions!");
ForgeHooks.writeCondition(pair.condition(), holder);
array.add(holder);
}
return json;
}
return null;
}
public static class Builder
{
private List<ICondition[]> conditions = new ArrayList<>();
private List<Supplier<JsonElement>> advancements = new ArrayList<>();
public interface IResult {
JsonObject serializeToJson();
}
private List<ICondition> currentConditions = new ArrayList<>();
private boolean locked = false;
public Builder addCondition(ICondition condition)
{
if (locked)
throw new IllegalStateException("Attempted to modify finished builder");
currentConditions.add(condition);
return this;
}
public Builder addAdvancement(Consumer<Consumer<Advancement.Builder>> callable)
{
if (locked)
throw new IllegalStateException("Attempted to modify finished builder");
callable.accept(this::addAdvancement);
return this;
}
public Builder addAdvancement(Advancement.Builder advancement)
{
return addAdvancement(advancement::serializeToJson);
}
public Builder addAdvancement(FinishedRecipe fromRecipe)
{
return addAdvancement(fromRecipe::serializeAdvancement);
}
private Builder addAdvancement(Supplier<JsonElement> jsonSupplier)
{
if (locked)
throw new IllegalStateException("Attempted to modify finished builder");
if (currentConditions.isEmpty())
throw new IllegalStateException("Can not add a advancement with no conditions.");
conditions.add(currentConditions.toArray(new ICondition[currentConditions.size()]));
advancements.add(jsonSupplier);
currentConditions.clear();
return this;
}
public JsonObject write()
{
if (!locked)
{
if (!currentConditions.isEmpty())
throw new IllegalStateException("Invalid builder state: Orphaned conditions");
if (advancements.isEmpty())
throw new IllegalStateException("Invalid builder state: No Advancements");
locked = true;
}
JsonObject json = new JsonObject();
JsonArray array = new JsonArray();
private record Pair(ICondition condition, Advancement adv) {}
private record Result(List<Pair> advancements) implements IResult {
@Override
public JsonObject serializeToJson() {
var json = new JsonObject();
var array = new JsonArray();
json.add("advancements", array);
for (int x = 0; x < conditions.size(); x++)
{
JsonObject holder = new JsonObject();
JsonArray conds = new JsonArray();
for (ICondition c : conditions.get(x))
conds.add(CraftingHelper.serialize(c));
holder.add("conditions", conds);
holder.add("advancement", advancements.get(x).get());
for (var pair : advancements) {
var holder = pair.adv().serializeToJson();
if (holder.has(ICondition.DEFAULT_FIELD))
throw new IllegalStateException("Recipe already serialized conditions!");
ForgeHooks.writeCondition(pair.condition(), holder);
array.add(holder);
}
@@ -128,4 +118,3 @@ public class ConditionalAdvancement
}
}
}
*/
@@ -4,188 +4,159 @@
*/
package net.minecraftforge.common.crafting;
/* TODO: Conditional Recipes
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.Dynamic;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.JsonOps;
import net.minecraft.Util;
import net.minecraft.advancements.Advancement;
import net.minecraft.advancements.AdvancementHolder;
import net.minecraft.data.recipes.FinishedRecipe;
import net.minecraft.data.recipes.RecipeBuilder;
import net.minecraft.data.recipes.RecipeOutput;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.RecipeManager;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.common.crafting.conditions.ConditionCodec;
import net.minecraftforge.common.crafting.conditions.ICondition;
import net.minecraftforge.registries.ObjectHolder;
import net.minecraftforge.registries.ForgeRegistries;
import org.jetbrains.annotations.Nullable;
public class ConditionalRecipe
{
@ObjectHolder(registryName = "recipe_serializer", value = "forge:conditional")
public static final RecipeSerializer<Recipe<?>> SERIALZIER = null;
public static Builder builder()
{
/**
* So, A 'ConditionalRecipe' differs from all normal recipes in the fact that in addition to the conditions
* disabling the entire recipe, it has sub-recipes that themselves have conditions.
*
* And when being deserialized it returns the first entry that passes the conditional check.
* This basically means that you can have muultiple variants all use the same recipe name, and
* only one will ever be loaded.
*
* This also means that you can wrap ALL recipes in a Conditional even those that don't explicitly
* have support for them in their data gen.
*/
public class ConditionalRecipe {
public static Builder builder() {
return new Builder();
}
public static class Serializer<T extends Recipe<?>> implements RecipeSerializer<T>
{
@Override
public T fromJson(ResourceLocation recipeId, JsonObject json)
{
return fromJson(recipeId, json, ICondition.IContext.EMPTY);
}
@SuppressWarnings("unchecked") // We return a nested one, so we can't know what type it is.
@Override
public T fromJson(ResourceLocation recipeId, JsonObject json, ICondition.IContext context)
{
JsonArray items = GsonHelper.getAsJsonArray(json, "recipes");
int idx = 0;
for (JsonElement ele : items)
{
if (!ele.isJsonObject())
throw new JsonSyntaxException("Invalid recipes entry at index " + idx + " Must be JsonObject");
if (CraftingHelper.processConditions(GsonHelper.getAsJsonArray(ele.getAsJsonObject(), "conditions"), context))
return (T)RecipeManager.fromJson(recipeId, GsonHelper.getAsJsonObject(ele.getAsJsonObject(), "recipe"));
idx++;
public static class Builder {
private List<RecipePair> recipes = new ArrayList<>();
private RecipeOutput bouncer = new RecipeOutput() {
@Override
public void accept(FinishedRecipe value) {
recipe(value);
}
return null;
}
//Should never get here as we return one of the recipes we wrap.
@Override public T fromNetwork(ResourceLocation recipeId, FriendlyByteBuf buffer) { return null; }
@Override public void toNetwork(FriendlyByteBuf buffer, T recipe) {}
}
@SuppressWarnings("removal")
@Override
public Advancement.Builder advancement() {
return Advancement.Builder.recipeAdvancement().parent(RecipeBuilder.ROOT_RECIPE_ADVANCEMENT);
}
};
public static class Builder
{
private List<ICondition[]> conditions = new ArrayList<>();
private List<FinishedRecipe> recipes = new ArrayList<>();
private ResourceLocation advId;
private ConditionalAdvancement.Builder adv;
@Nullable
private ICondition condition;
@Nullable
private ICondition mainCondition;
private List<ICondition> currentConditions = new ArrayList<>();
@Nullable
private ResourceLocation advancementId;
public Builder addCondition(ICondition condition)
{
currentConditions.add(condition);
public Builder mainCondition(ICondition value) {
if (this.mainCondition != null)
throw new IllegalStateException("Attempted to overrride the main condition, only one is allowed to be set");
this.mainCondition = value;
return this;
}
public Builder addRecipe(Consumer<Consumer<FinishedRecipe>> callable)
{
callable.accept(this::addRecipe);
public Builder condition(ICondition value) {
if (this.condition != null)
throw new IllegalStateException("Attempted to override a previous set condition before adding a recipe");
this.condition = value;
return this;
}
public Builder addRecipe(FinishedRecipe recipe)
{
if (currentConditions.isEmpty())
public Builder recipe(Consumer<RecipeOutput> callable) {
callable.accept(bouncer);
return this;
}
public Builder recipe(FinishedRecipe recipe) {
if (condition == null)
throw new IllegalStateException("Can not add a recipe with no conditions.");
conditions.add(currentConditions.toArray(new ICondition[currentConditions.size()]));
recipes.add(recipe);
currentConditions.clear();
recipes.add(new RecipePair(this.condition, recipe));
condition = null;
return this;
}
public Builder generateAdvancement()
{
return generateAdvancement(null);
}
public Builder generateAdvancement(@Nullable ResourceLocation id)
{
ConditionalAdvancement.Builder builder = ConditionalAdvancement.builder();
for(int i=0;i<recipes.size();i++)
{
for(ICondition cond : conditions.get(i))
builder = builder.addCondition(cond);
builder = builder.addAdvancement(recipes.get(i));
}
return setAdvancement(id, builder);
}
public Builder setAdvancement(ConditionalAdvancement.Builder advancement)
{
return setAdvancement(null, advancement);
}
public Builder setAdvancement(String namespace, String path, ConditionalAdvancement.Builder advancement)
{
return setAdvancement(new ResourceLocation(namespace, path), advancement);
}
public Builder setAdvancement(@Nullable ResourceLocation id, ConditionalAdvancement.Builder advancement)
{
if (this.adv != null)
throw new IllegalStateException("Invalid ConditionalRecipeBuilder, Advancement already set");
this.advId = id;
this.adv = advancement;
public Builder advancement(ResourceLocation id) {
this.advancementId = id;
return this;
}
public void build(Consumer<FinishedRecipe> consumer, String namespace, String path)
{
build(consumer, new ResourceLocation(namespace, path));
public void save(RecipeOutput out, String namespace, String path) {
save(out, new ResourceLocation(namespace, path));
}
public void build(Consumer<FinishedRecipe> consumer, ResourceLocation id)
{
if (!currentConditions.isEmpty())
public void save(RecipeOutput out, ResourceLocation id) {
if (condition != null)
throw new IllegalStateException("Invalid ConditionalRecipe builder, Orphaned conditions");
if (recipes.isEmpty())
throw new IllegalStateException("Invalid ConditionalRecipe builder, No recipes");
if (advId == null && adv != null)
{
advId = new ResourceLocation(id.getNamespace(), "recipes/" + id.getPath());
var adv = ConditionalAdvancement.builder();
var hasAdvancement = false;
for (var pair : recipes) {
if (pair.recipe().advancement() != null) {
adv.condition(pair.condition())
.advancement(pair.recipe().advancement());
hasAdvancement = true;
}
}
consumer.accept(new Finished(id, conditions, recipes, advId, adv));
if (advancementId == null)
advancementId = id.withPrefix("recipes/");
out.accept(new Finished(id, mainCondition, recipes, advancementId, hasAdvancement ? adv.write() : null));
}
}
private static class Finished implements FinishedRecipe
{
private final ResourceLocation id;
private final List<ICondition[]> conditions;
private final List<FinishedRecipe> recipes;
private final ResourceLocation advId;
private final ConditionalAdvancement.Builder adv;
private Finished(ResourceLocation id, List<ICondition[]> conditions, List<FinishedRecipe> recipes, @Nullable ResourceLocation advId, @Nullable ConditionalAdvancement.Builder adv)
{
this.id = id;
this.conditions = conditions;
this.recipes = recipes;
this.advId = advId;
this.adv = adv;
}
private record Finished(
ResourceLocation id,
@Nullable
ICondition mainCondition,
List<RecipePair> recipes,
ResourceLocation advId,
JsonObject advData
) implements FinishedRecipe {
@Override
public void serializeRecipeData(JsonObject json) {
JsonArray array = new JsonArray();
var main = mainCondition;
if (main == null && recipes.size() == 1)
main = recipes.get(0).condition();
ForgeHooks.writeCondition(main, json);
json.add("recipes", array);
for (int x = 0; x < conditions.size(); x++)
{
JsonObject holder = new JsonObject();
JsonArray conds = new JsonArray();
for (ICondition c : conditions.get(x))
conds.add(CraftingHelper.serialize(c));
holder.add("conditions", conds);
holder.add("recipe", recipes.get(x).serializeRecipe());
for (var pair : recipes) {
var holder = pair.recipe().serializeRecipe();
if (holder.has(ICondition.DEFAULT_FIELD))
throw new IllegalStateException("Recipe already serialized conditions!");
ForgeHooks.writeCondition(pair.condition(), holder);
array.add(holder);
}
}
@@ -196,22 +167,77 @@ public class ConditionalRecipe
}
@Override
public RecipeSerializer<?> type()
{
return SERIALZIER;
public RecipeSerializer<?> type() {
return ConditionalRecipe.SERIALZIER;
}
@Nullable
@Override
public AdvancementHolder advancement() {
if (adv == null)
return null;
return new AdvancementHolder(advId, adv.)
return null;
}
@Nullable
@Override
public AdvancementData advancementData() {
return advData == null ? null : new AdvancementData(advId, advData);
}
}
private record RecipePair(ICondition condition, FinishedRecipe recipe) {}
private static Codec<Recipe<?>> CODEC = new Codec<Recipe<?>>() {
@Override
public <T> DataResult<T> encode(Recipe<?> input, DynamicOps<T> ops, T prefix) {
throw new UnsupportedOperationException("ConditionRecipe.CODEC does not support encoding");
}
@Override
public ResourceLocation getAdvancementId() {
return advId;
public <T> DataResult<Pair<Recipe<?>, T>> decode(DynamicOps<T> ops, T input) {
var context = ConditionCodec.getContext(ops);
var json = new Dynamic<>(ops, input).convert(JsonOps.INSTANCE).getValue();
try {
var recipes = GsonHelper.getAsJsonArray(GsonHelper.convertToJsonObject(json.getAsJsonObject(), "root"), "recipes");
int idx = 0;
for (var entry : recipes) {
var object = GsonHelper.convertToJsonObject(entry, "recipe[" + idx++ + "]");
var conditionjson = GsonHelper.getAsJsonObject(object, ICondition.DEFAULT_FIELD);
var condition = Util.getOrThrow(ICondition.SAFE_CODEC.parse(JsonOps.INSTANCE, conditionjson), JsonSyntaxException::new);
if (!condition.test(context))
continue;
var type = new ResourceLocation(GsonHelper.getAsString(object, "type"));
var serializer = ForgeRegistries.RECIPE_SERIALIZERS.getValue(type);
if (serializer == null)
return DataResult.error(() -> "Invalid or unsupported recipe type '" + type + "' Conditions were successful, but unknown type");
var parsed = serializer.codec().parse(JsonOps.INSTANCE, object);
if (parsed.error().isPresent())
return DataResult.error(parsed.error().get()::message);
else if (!parsed.result().isPresent())
return DataResult.error(() -> "Recipe passed all conditions but did not parse a valid return");
else
return DataResult.success(Pair.of(parsed.result().get(), ops.empty()));
}
return DataResult.error(() -> "No recipe passed conditions, if this is the case, you should have an outer condition.");
} catch (JsonSyntaxException e) {
return DataResult.error(() -> e.getMessage());
}
}
}
}.stable();
public static final RecipeSerializer<Recipe<?>> SERIALZIER = new RecipeSerializer<Recipe<?>>() {
@Override
public Codec<Recipe<?>> codec() {
return CODEC;
}
@Override
public Recipe<?> fromNetwork(FriendlyByteBuf buffer) {
throw new UnsupportedOperationException("ConditionaRecipe.SERIALIZER does not support decoding from network");
}
@Override
public void toNetwork(FriendlyByteBuf buffer, Recipe<?> recipe) {
throw new UnsupportedOperationException("ConditionaRecipe.SERIALIZER does not support encoding to network");
}
};
}
*/
@@ -14,15 +14,18 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraftforge.common.crafting.conditions.ICondition;
import net.minecraft.nbt.TagParser;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.util.GsonHelper;
import net.minecraft.resources.ResourceLocation;
@@ -171,30 +174,4 @@ public class CraftingHelper {
return new ItemStack(item, GsonHelper.getAsInt(json, "count", 1));
}
// CONDITIONS
private static final Map<ResourceLocation, Codec<? extends ICondition>> conditions = new HashMap<>();
public static void register(ResourceLocation key, Codec<? extends ICondition> codec) {
if (conditions.containsKey(key))
throw new IllegalStateException("Duplicate recipe condition serializer: " + key);
conditions.put(key, codec);
}
public static boolean processConditions(JsonObject json, String memberName, ICondition.IContext context) {
return !json.has(memberName) || processConditions(GsonHelper.getAsJsonArray(json, memberName), context);
}
public static boolean processConditions(JsonArray conditions, ICondition.IContext context) {
for (int x = 0; x < conditions.size(); x++) {
if (!conditions.get(x).isJsonObject())
throw new JsonSyntaxException("Conditions must be an array of JsonObjects");
JsonObject json = conditions.get(x).getAsJsonObject();
/*
if (!CraftingHelper.getCondition(json).test(context))
return false;
*/
}
return true;
}
}
@@ -0,0 +1,166 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.common.crafting;
import java.util.ArrayList;
import java.util.List;
import it.unimi.dsi.fastutil.chars.Char2ObjectMap;
import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.chars.CharArraySet;
import net.minecraft.core.NonNullList;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.player.StackedContents;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.ItemLike;
public class SimpleCraftingContainer implements CraftingContainer {
private final int width;
private final int height;
private final NonNullList<ItemStack> items;
public static Builder builder() {
return new Builder();
}
public SimpleCraftingContainer(int width, int height) {
this(width, height, NonNullList.withSize(width * height, ItemStack.EMPTY));
}
public SimpleCraftingContainer(int width, int height, NonNullList<ItemStack> items) {
this.width = width;
this.height = height;
this.items = items;
if (items.size() != (width * height))
throw new IllegalArgumentException("Invalid item list, must be same size inventory width * height, received " + items.size() + " expected " + (width * height));
}
@Override
public int getContainerSize() {
return this.items.size();
}
@Override
public boolean isEmpty() {
return this.items.stream().noneMatch(p -> !p.isEmpty());
}
@Override
public ItemStack getItem(int slot) {
return slot >= this.items.size() ? ItemStack.EMPTY : this.items.get(slot);
}
@Override
public ItemStack removeItem(int slot, int count) {
return ContainerHelper.removeItem(this.items, slot, count);
}
@Override
public ItemStack removeItemNoUpdate(int slot) {
return ContainerHelper.takeItem(this.items, slot);
}
@Override
public void setItem(int slot, ItemStack stack) {
this.items.set(slot, stack);
}
@Override
public void setChanged() {
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public void clearContent() {
this.items.clear();
}
@Override
public void fillStackedContents(StackedContents stacked) {
this.items.forEach(stacked::accountSimpleStack);
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
@Override
public List<ItemStack> getItems() {
return List.copyOf(this.items);
}
public static class Builder {
private final List<String> rows = new ArrayList<>();
private final Char2ObjectMap<ItemStack> keys = new Char2ObjectOpenHashMap<>();
private Builder() {
this.define(' ', ItemStack.EMPTY);
}
public Builder pattern(String row) {
if (!this.rows.isEmpty() && row.length() != this.rows.get(0).length())
throw new IllegalArgumentException("Pattern must be the same width on every line");
this.rows.add(row);
return this;
}
public Builder pattern(String... rows) {
for (var row : rows)
pattern(row);
return this;
}
public Builder define(char key, ItemLike item) {
return define(key, new ItemStack(item));
}
public Builder define(char key, ItemStack stack) {
if (this.keys.containsKey(key))
throw new IllegalArgumentException("key '" + key + "' is already defined.");
this.keys.put(key, stack);
return this;
}
public SimpleCraftingContainer build() {
var unseen = new CharArraySet(this.keys.keySet());
unseen.remove(' ');
int height = this.rows.size();
if (height == 0)
throw new IllegalStateException("Invalid builder, empty inventory");
int width = this.rows.get(0).length();
var items = NonNullList.withSize(width * height, ItemStack.EMPTY);
int idx = 0;
for (var row : this.rows) {
for (int x = 0; x < width; x++) {
char key = row.charAt(x);
var stack = this.keys.get(key);
if (stack == null)
throw new IllegalStateException("Invalid builder pattern, missing value for key '" + key + "'");
unseen.remove(key);
items.set(idx++, stack.copy());
}
}
if (!unseen.isEmpty())
throw new IllegalStateException("Invalid builder, missing usage of defined keys: " + unseen);
return new SimpleCraftingContainer(width, height, items);
}
}
}
@@ -27,7 +27,7 @@ public record AndCondition(List<ICondition> children) implements ICondition {
@Override
public String toString() {
return Joiner.on(" && ").join(children);
return '(' + Joiner.on(" && ").join(children) + ')';
}
@Override
@@ -29,6 +29,7 @@ public interface ICondition {
Codec<ICondition> CODEC = ExtraCodecs.lazyInitializedCodec(() -> ForgeRegistries.CONDITION_SERIALIZERS.get().getCodec().dispatch(ICondition::codec, Function.identity()));
String DEFAULT_FIELD = "forge:condition";
MapCodec<Optional<ICondition>> OPTIONAL_FEILD_CODEC = CODEC.optionalFieldOf(DEFAULT_FIELD);
Codec<ICondition> SAFE_CODEC = CODEC.orElse(FalseCondition.INSTANCE);
boolean test(IContext context);
@@ -12,9 +12,9 @@ import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
public record OrCondition(List<ICondition> children) implements ICondition {
public static final Codec<AndCondition> CODEC = RecordCodecBuilder.create(b -> b.group(
ICondition.CODEC.listOf().fieldOf("values").forGetter(AndCondition::children)
).apply(b, AndCondition::new));
public static final Codec<OrCondition> CODEC = RecordCodecBuilder.create(b -> b.group(
ICondition.CODEC.listOf().fieldOf("values").forGetter(OrCondition::children)
).apply(b, OrCondition::new));
@Override
@@ -29,7 +29,7 @@ public record OrCondition(List<ICondition> children) implements ICondition {
@Override
public String toString() {
return Joiner.on(" || ").join(children);
return '(' + Joiner.on(" || ").join(children) + ')';
}
@Override
@@ -133,7 +133,7 @@ public final class ForgeRecipeProvider extends VanillaRecipeProvider {
@Nullable
@Override
protected CompletableFuture<?> saveAdvancement(CachedOutput output, JsonObject advancementJson, AdvancementHolder holder, FinishedRecipe finishedRecipe) {
protected CompletableFuture<?> saveAdvancement(CachedOutput output, ResourceLocation advancementId, JsonObject advancementJson, FinishedRecipe finishedRecipe) {
// NOOP - We don't replace any of the advancement things yet...
return null;
}
@@ -6,10 +6,12 @@
package net.minecraftforge.common.extensions;
import java.util.UUID;
import java.util.function.Supplier;
import com.mojang.authlib.GameProfile;
import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.gametest.framework.GameTestAssertException;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.network.Connection;
import net.minecraft.network.ConnectionProtocol;
@@ -23,6 +25,16 @@ public interface IForgeGameTestHelper {
return (GameTestHelper)this;
}
default void assertTrue(boolean value, Supplier<String> message) {
if (!value)
throw new GameTestAssertException(message.get());
}
default void assertFalse(boolean value, Supplier<String> message) {
if (value)
throw new GameTestAssertException(message.get());
}
default ServerPlayer makeMockServerPlayer() {
var level = self().getLevel();
var cookie = CommonListenerCookie.createInitial(new GameProfile(UUID.randomUUID(), "test-mock-player"));