Skip to content

Commit

Permalink
Provide recipe to IReplacementRules for more complex behavior
Browse files Browse the repository at this point in the history
Breaking

Signed-off-by: TheSilkMiner <thesilkminer@outlook.com>
  • Loading branch information
TheSilkMiner committed May 29, 2021
1 parent 75eb97a commit 03e5dcb
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -154,24 +154,28 @@ public ReplacementNotSupportedException(final String message, final Throwable ca
*
* <p>The value of {@code type} represents the class type of the ingredient. Its value should be the most general
* class type possible that the recipe can accept (e.g., a recipe that can accept any form of ingredient would
* specify either {@code IIngredient} or {@code Ingredient} as the value for {@code type}.</p>
* specify either {@code IIngredient} or {@code Ingredient} as the value for {@code type}).</p>
*
* @param ingredient The ingredient that should undergo replacement.
* @param type The actual class type of the ingredient, or one of its superclasses, as determined by the client.
* @param recipe The recipe whose ingredients are currently undergoing replacement; or {@code null} if no valid
* recipe can be provided.
* @param rules A series of {@link IReplacementRule}s in the order they should be applied.
* @param <S> The type of the recipe whose ingredients are currently undergoing replacement. The given type must be
* a subtype of {@link IRecipe}. If no valid type exists, then {@link IRecipe} is assumed.
* @param <U> The type of the ingredient that should undergo replacement. No restrictions are placed on the type of
* the ingredient.
* @return An {@link Optional} holding the replaced ingredient, if any replacements have been carried out. If no
* replacement rule affected the current ingredient, the return value should be {@link Optional#empty()}. It is
* customary, though not required, that the value wrapped by the optional is a completely different object from
* {@code ingredient} (i.e. {@code ingredient != result.get()}).
*/
static <U> Optional<U> attemptReplacing(final U ingredient, final Class<U> type, final List<IReplacementRule> rules) {
static <S extends IRecipe<?>, U> Optional<U> attemptReplacing(final U ingredient, final Class<U> type, final S recipe, final List<IReplacementRule> rules) {
final BinaryOperator<Optional<U>> combiner = (oldOpt, newOpt) -> newOpt.isPresent()? newOpt : oldOpt;
return rules.stream()
.reduce(
Optional.empty(),
(optional, rule) -> combiner.apply(optional, rule.getReplacement(optional.orElse(ingredient), type)),
(optional, rule) -> combiner.apply(optional, rule.getReplacement(optional.orElse(ingredient), type, recipe)),
combiner
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.blamejared.crafttweaker.api.recipes;

import net.minecraft.item.crafting.IRecipe;

import java.util.Arrays;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.BiFunction;

/**
* Represents a rule used for replacement of various ingredients inside a recipe.
Expand All @@ -16,12 +18,13 @@ public interface IReplacementRule {
/**
* Represents a rule that does nothing.
*
* <p>This replacement rule simply returns {@link Optional#empty()} in {@link #getReplacement(Object, Class)} for
* any possible set of parameters, representing effectively a no-op replacement rule.</p>
* <p>This replacement rule simply returns {@link Optional#empty()} in
* {@link #getReplacement(Object, Class, IRecipe)} for any possible set of parameters, representing effectively a
* no-op replacement rule.</p>
*/
IReplacementRule EMPTY = new IReplacementRule() {
@Override
public <T> Optional<T> getReplacement(final T initial, final Class<T> type) {
public <T, U extends IRecipe<?>> Optional<T> getReplacement(T ingredient, Class<T> type, U recipe) {
return Optional.empty();
}

Expand All @@ -38,7 +41,7 @@ public String describe() {
* @param <T> The type parameter of the various optionals.
* @return The first non-empty optional, if present, or {@link Optional#empty()} otherwise.
*
* @see #withType(Object, Class, Class, Function)
* @see #withType(Object, Class, IRecipe, Class, BiFunction)
*/
@SafeVarargs
static <T> Optional<T> chain(final Optional<T>... optionals) {
Expand All @@ -56,27 +59,33 @@ static <T> Optional<T> chain(final Optional<T>... optionals) {
* operate on the ingredient, and {@link Optional#empty()} is returned.</p>
*
* @param ingredient The ingredient that should be replaced; its value <strong>should</strong> match the input of
* {@link #getReplacement(Object, Class)}.
* {@link #getReplacement(Object, Class, IRecipe)}.
* @param type The type of the {@code ingredient} that should be replaced; its value <strong>should</strong> match
* the input of {@link #getReplacement(Object, Class)}.
* the input of {@link #getReplacement(Object, Class, IRecipe)}.
* @param recipe The recipe that is currently being acted upon, or {@code null} if this information cannot be
* provided; its value <strong>should</strong> match the input of
* {@link #getReplacement(Object, Class, IRecipe)}.
* @param targetedType The type the ingredient should have for it to be operated upon by the {@code producer}. This
* value will be compared to {@code type} with a direct equality check (i.e.
* {@code type == targetedType}).
* @param producer A {@link Function} that takes an ingredient of type {@code targetedType} as an input and replaces
* it, returning either an {@link Optional} with the ingredient, or {@link Optional#empty()} if the
* ingredient cannot be replaced.
* @param producer A {@link BiFunction} that takes an ingredient of type {@code targetedType} and the targeted
* recipe as an input and replaces the ingredient, returning either an {@link Optional} with the
* ingredient, or {@link Optional#empty()} if the ingredient cannot be replaced.
* @param <T> The type of the ingredient that is passed to the function; its value <strong>should</strong> match the
* one of {@link #getReplacement(Object, Class)}.
* one of {@link #getReplacement(Object, Class, IRecipe)}.
* @param <U> The type of the ingredient that the {@code producer} recognizes.
* @param <S> The type of the recipe that is currently being replaced; its value <strong>should</strong> match the
* one of {@link #getReplacement(Object, Class, IRecipe)}.
* @return An {@link Optional} containing the replaced ingredient, if {@code type} matches {@code targetedType} and
* {@code producer} determines that a replacement of the ingredient is needed; {@link Optional#empty()} in all other
* cases.
*
* @see #chain(Optional[])
*/
static <T, U> Optional<T> withType(final T ingredient, final Class<T> type, final Class<U> targetedType, final Function<U, Optional<U>> producer) {
static <T, U, S extends IRecipe<?>> Optional<T> withType(final T ingredient, final Class<T> type, final S recipe,
final Class<U> targetedType, final BiFunction<U, S, Optional<U>> producer) {
// Those casts are effectively no-ops
return targetedType == type? producer.apply(targetedType.cast(ingredient)).map(type::cast) : Optional.empty();
return targetedType == type? producer.apply(targetedType.cast(ingredient), recipe).map(type::cast) : Optional.empty();
}

/**
Expand All @@ -91,11 +100,13 @@ static <T, U> Optional<T> withType(final T ingredient, final Class<T> type, fina
* ingredient's class, although it's guaranteed to be one of its superclasses (in other words, it is
* guaranteed that {@code type.isAssignableFrom(ingredient.getClass())}, but the equality check
* {@code type == ingredient.getClass()} is not guaranteed).
* @param recipe The recipe that is currently being subjected to replacement, if any; {@code null} otherwise.
* @param <T> The type of the ingredient that should be replaced.
* @param <U> The type of the recipe that is currently being replaced.
* @return An {@link Optional} containing the replaced ingredient, if this rule knows how to operate on the
* ingredient's type and deems that the ingredient should be replaced; {@link Optional#empty()} otherwise.
*/
<T> Optional<T> getReplacement(final T ingredient, final Class<T> type);
<T, U extends IRecipe<?>> Optional<T> getReplacement(final T ingredient, final Class<T> type, final U recipe);

/**
* Describes in a short and simple sentence the behavior of this rule.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
import java.util.function.Function;
import java.util.stream.IntStream;

/**
* Set of helper functions that can replace ingredients according to the given {@link IReplacementRule}s.
*
* @see IReplacementRule
* @see IRecipeHandler#attemptReplacing(Object, Class, IRecipe, List)
*/
public final class ReplacementHandlerHelper {
private static final class ReplacementMap<T> {
private interface ReplacementConsumer<T> {
Expand Down Expand Up @@ -52,16 +58,72 @@ boolean isEmpty() {

private ReplacementHandlerHelper() {}

/**
* Replaces the given {@link NonNullList} of ingredients of type {@code ingredientClass} according to the given set
* of rules.
*
* <p>This acts as the {@code NonNullList} equivalent of
* {@link #replaceIngredientList(List, Class, IRecipe, List, Function)}, in case the recipe constructor explicitly
* requires a {@code NonNullList} to be passed as a parameter.</p>
*
* @param originalIngredients The original {@link NonNullList} of ingredients that will be replaced; this
* <strong>should</strong> match the list of ingredients in the recipe.
* @param ingredientClass The type of the ingredient that should be replaced. Its value may or may not correspond to
* the actual ingredient's class, although it must be one of its superclass (in other words,
* it is not necessary for {@code ingredientClass == originalIngredients[i].getClass()} for
* any {@code i} between {@code 0} and {@code originalIngredients.size()}; on the other hand,
* {@code ingredientClass.isAssignableFrom(originalIngredients[i].getClass())} must hold true
* for every {@code i} in the same range).
* @param recipe The recipe whose ingredients are currently undergoing replacement.
* @param rules The list of {@link IReplacementRule}s that need to be applied to the recipe.
* @param factory A {@link Function} that accepts a {@link NonNullList} of ingredients of type {@code U} as a first
* parameter and returns a {@link Function} that can create the recipe given an ID in the form of
* a {@link ResourceLocation} (it effectively is a factory for a recipe factory).
* @param <T> The type of the recipe whose ingredients are currently undergoing replacement.
* @param <U> The type of the ingredient that is currently being replaced. No restrictions are placed on the type
* of the ingredient.
* @return An {@link Optional} holding a {@link Function} that is able to create a new recipe with the replaced
* ingredient list when given an ID in the form of a {@link ResourceLocation}, if any replacements have been carried
* out on one or more of its ingredients; {@link Optional#empty()} otherwise.
*/
public static <T extends IRecipe<?>, U> Optional<Function<ResourceLocation, T>> replaceNonNullIngredientList(final NonNullList<U> originalIngredients, final Class<U> ingredientClass,
final List<IReplacementRule> rules,
final T recipe, final List<IReplacementRule> rules,
final Function<NonNullList<U>, Function<ResourceLocation, T>> factory) {
return replaceIngredientList(originalIngredients, ingredientClass, rules, list -> factory.apply(Util.make(NonNullList.create(), it -> it.addAll(list))));
return replaceIngredientList(originalIngredients, ingredientClass, recipe, rules, list -> factory.apply(Util.make(NonNullList.create(), it -> it.addAll(list))));
}

/**
* Replaces the given {@link List} of ingredients of type {@code ingredientClass} according to the given set of
* rules.
*
* <p>If the recipe requires a {@link NonNullList} as a parameter, it is possible to use the more specialized
* version {@link #replaceNonNullIngredientList(NonNullList, Class, IRecipe, List, Function)}.</p>
*
* @param originalIngredients The original {@link List} of ingredients that will be replaced; this
* <strong>should</strong> match the list of ingredients in the recipe.
* @param ingredientClass The type of the ingredient that should be replaced. Its value may or may not correspond to
* the actual ingredient's class, although it must be one of its superclass (in other words,
* it is not necessary for {@code ingredientClass == originalIngredients[i].getClass()} for
* any {@code i} between {@code 0} and {@code originalIngredients.size()}; on the other hand,
* {@code ingredientClass.isAssignableFrom(originalIngredients[i].getClass())} must hold true
* for every {@code i} in the same range).
* @param recipe The recipe whose ingredients are currently undergoing replacement.
* @param rules The list of {@link IReplacementRule}s that need to be applied to the recipe.
* @param factory A {@link Function} that accepts a {@link List} of ingredients of type {@code U} as a first
* parameter and returns a {@link Function} that can create the recipe given an ID in the form of
* a {@link ResourceLocation} (it effectively is a factory for a recipe factory).
* @param <T> The type of the recipe whose ingredients are currently undergoing replacement.
* @param <U> The type of the ingredient that is currently being replaced. No restrictions are placed on the type
* of the ingredient.
* @return An {@link Optional} holding a {@link Function} that is able to create a new recipe with the replaced
* ingredient list when given an ID in the form of a {@link ResourceLocation}, if any replacements have been carried
* out on one or more of its ingredients; {@link Optional#empty()} otherwise.
*/
public static <T extends IRecipe<?>, U> Optional<Function<ResourceLocation, T>> replaceIngredientList(final List<U> originalIngredients, final Class<U> ingredientClass,
final List<IReplacementRule> rules, final Function<List<U>, Function<ResourceLocation, T>> factory) {
final T recipe, final List<IReplacementRule> rules,
final Function<List<U>, Function<ResourceLocation, T>> factory) {
final ReplacementMap<U> replacements = IntStream.range(0, originalIngredients.size())
.mapToObj(i -> Pair.of(i, IRecipeHandler.attemptReplacing(originalIngredients.get(i), ingredientClass, rules)))
.mapToObj(i -> Pair.of(i, IRecipeHandler.attemptReplacing(originalIngredients.get(i), ingredientClass, recipe, rules)))
.filter(it -> it.getSecond().isPresent())
.collect(ReplacementMap::new, ReplacementMap::put, ReplacementMap::merge);

Expand All @@ -74,11 +136,34 @@ public static <T extends IRecipe<?>, U> Optional<Function<ResourceLocation, T>>
return Optional.of(factory.apply(newIngredients));
}

/**
* Replaces the given array of ingredients of type {@code ingredientClass} according to the given set of rules.
*
* @param originalIngredients The original array of ingredients that will be replaced; this <strong>should</strong>
* match the array of ingredients in the recipe.
* @param ingredientClass The type of the ingredient that should be replaced. Its value may or may not correspond to
* the actual ingredient's class, although it must be one of its superclass (in other words,
* it is not necessary for {@code ingredientClass == originalIngredients[i].getClass()} for
* any {@code i} between {@code 0} and {@code originalIngredients.size()}; on the other hand,
* {@code ingredientClass.isAssignableFrom(originalIngredients[i].getClass())} must hold true
* for every {@code i} in the same range).
* @param recipe The recipe whose ingredients are currently undergoing replacement.
* @param rules The list of {@link IReplacementRule}s that need to be applied to the recipe.
* @param factory A {@link Function} that accepts an array of ingredients of type {@code U} as a first parameter and
* returns a {@link Function} that can create the recipe given an ID in the form of a
* {@link ResourceLocation} (it effectively is a factory for a recipe factory).
* @param <T> The type of the recipe whose ingredients are currently undergoing replacement.
* @param <U> The type of the ingredient that is currently being replaced. No restrictions are placed on the type
* of the ingredient.
* @return An {@link Optional} holding a {@link Function} that is able to create a new recipe with the replaced
* ingredient list when given an ID in the form of a {@link ResourceLocation}, if any replacements have been carried
* out on one or more of its ingredients; {@link Optional#empty()} otherwise.
*/
public static <T extends IRecipe<?>, U> Optional<Function<ResourceLocation, T>> replaceIngredientArray(final U[] originalIngredients, final Class<U> ingredientClass,
final List<IReplacementRule> rules,
final T recipe, final List<IReplacementRule> rules,
final Function<U[], Function<ResourceLocation, T>> factory) {
final ReplacementMap<U> replacements = IntStream.range(0, originalIngredients.length)
.mapToObj(i -> Pair.of(i, IRecipeHandler.attemptReplacing(originalIngredients[i], ingredientClass, rules)))
.mapToObj(i -> Pair.of(i, IRecipeHandler.attemptReplacing(originalIngredients[i], ingredientClass, recipe, rules)))
.filter(it -> it.getSecond().isPresent())
.collect(ReplacementMap::new, ReplacementMap::put, ReplacementMap::merge);

Expand Down
Loading

0 comments on commit 03e5dcb

Please sign in to comment.