Fix replacement map and handle duplicates

This commit is contained in:
LLytho 2022-07-15 12:53:01 +02:00
parent 9ecf9ff1c9
commit 2a61885889
19 changed files with 295 additions and 227 deletions

1
.gitignore vendored
View file

@ -20,5 +20,6 @@ build
# other # other
*.log *.log
*.log.gz
eclipse eclipse
run run

View file

@ -9,11 +9,11 @@ public interface AlmostUnifiedPlatform {
AlmostUnifiedPlatform INSTANCE = PlatformLoader.load(AlmostUnifiedPlatform.class); AlmostUnifiedPlatform INSTANCE = PlatformLoader.load(AlmostUnifiedPlatform.class);
/** /**
* Gets the name of the current platform * Gets the current platform
* *
* @return The name of the current platform. * @return The current platform.
*/ */
String getPlatformName(); Platform getPlatform();
/** /**
* Checks if a mod with the given id is loaded. * Checks if a mod with the given id is loaded.

View file

@ -35,7 +35,7 @@ public abstract class AlmostUnifiedRuntime {
onRun(); onRun();
List<UnifyTag<Item>> allowedTags = config.getAllowedTags(); List<UnifyTag<Item>> allowedTags = config.getAllowedTags();
TagMap tagMap = createTagMap(allowedTags); TagMap tagMap = createTagMap(allowedTags);
ReplacementMap replacementMap = new ReplacementMap(tagMap, modPriorities); ReplacementMap replacementMap = new ReplacementMap(tagMap, modPriorities, config.getStoneStrata());
RecipeTransformer transformer = new RecipeTransformer(recipeHandlerFactory, replacementMap); RecipeTransformer transformer = new RecipeTransformer(recipeHandlerFactory, replacementMap);
RecipeTransformationResult result = transformer.transformRecipes(recipes); RecipeTransformationResult result = transformer.transformRecipes(recipes);
new RecipeDumper(result).dump(); new RecipeDumper(result).dump();

View file

@ -15,6 +15,13 @@ import java.util.*;
public class ModConfig { public class ModConfig {
public static final List<String> DEFAULT_STONE_STRATA = List.of("stone",
"nether",
"deepslate",
"granite",
"diorite",
"andesite");
@SuppressWarnings("SpellCheckingInspection") @SuppressWarnings("SpellCheckingInspection")
public static final List<String> DEFAULT_MOD_PRIORITIES = List.of( public static final List<String> DEFAULT_MOD_PRIORITIES = List.of(
"kubejs", "kubejs",
@ -69,8 +76,6 @@ public class ModConfig {
"lapis", "lapis",
"lead", "lead",
"lumium", "lumium",
"mana",
"manyullyn",
"nickel", "nickel",
"obsidian", "obsidian",
"osmium", "osmium",
@ -93,6 +98,7 @@ public class ModConfig {
"gears", "gears",
"gems", "gems",
"ingots", "ingots",
"raw_materials",
"ores", "ores",
"plates", "plates",
"rods", "rods",
@ -118,7 +124,7 @@ public class ModConfig {
} }
private static List<String> getDefaultPatterns() { private static List<String> getDefaultPatterns() {
if (AlmostUnifiedPlatform.INSTANCE.getPlatformName().equals("Forge")) { if (AlmostUnifiedPlatform.INSTANCE.getPlatform().equals("Forge")) {
return List.of("forge:{types}/{materials}"); return List.of("forge:{types}/{materials}");
} else { } else {
return List.of("c:{materials}_{types}"); return List.of("c:{materials}_{types}");
@ -156,6 +162,10 @@ public class ModConfig {
currentConfig.close(); currentConfig.close();
} }
public List<String> getStoneStrata() {
return Collections.unmodifiableList(DEFAULT_STONE_STRATA);
}
public List<String> getModPriorities() { public List<String> getModPriorities() {
if (currentConfig == null) { if (currentConfig == null) {
throw new IllegalStateException("Config is not loaded"); throw new IllegalStateException("Config is not loaded");

View file

@ -0,0 +1,6 @@
package com.almostreliable.unified;
public enum Platform {
Forge,
Fabric
}

View file

@ -2,12 +2,11 @@ package com.almostreliable.unified.api.recipe;
import com.almostreliable.unified.utils.UnifyTag; import com.almostreliable.unified.utils.UnifyTag;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.function.UnaryOperator; import java.util.function.Predicate;
public interface RecipeContext { public interface RecipeContext {
@ -15,7 +14,7 @@ public interface RecipeContext {
ResourceLocation getReplacementForItem(@Nullable ResourceLocation item); ResourceLocation getReplacementForItem(@Nullable ResourceLocation item);
@Nullable @Nullable
ResourceLocation getPreferredItemByTag(@Nullable UnifyTag<Item> tag); ResourceLocation getPreferredItemByTag(@Nullable UnifyTag<Item> tag, Predicate<ResourceLocation> filter);
@Nullable @Nullable
UnifyTag<Item> getPreferredTagByItem(@Nullable ResourceLocation item); UnifyTag<Item> getPreferredTagByItem(@Nullable ResourceLocation item);

View file

@ -13,6 +13,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.function.Predicate;
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
public class RecipeContextImpl implements RecipeContext { public class RecipeContextImpl implements RecipeContext {
@ -38,12 +39,12 @@ public class RecipeContextImpl implements RecipeContext {
@Nullable @Nullable
@Override @Override
public ResourceLocation getPreferredItemByTag(@Nullable UnifyTag<Item> tag) { public ResourceLocation getPreferredItemByTag(@Nullable UnifyTag<Item> tag, Predicate<ResourceLocation> filter) {
if (tag == null) { if (tag == null) {
return null; return null;
} }
return replacementMap.getPreferredItemByTag(tag); return replacementMap.getPreferredItemForTag(tag, filter);
} }
@Nullable @Nullable
@ -53,7 +54,7 @@ public class RecipeContextImpl implements RecipeContext {
return null; return null;
} }
return replacementMap.getPreferredTag(item); return replacementMap.getPreferredTagForItem(item);
} }
@Override @Override

View file

@ -9,6 +9,7 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
public class RecipeDumper { public class RecipeDumper {
@ -54,16 +55,22 @@ public class RecipeDumper {
private void dumpTransformedRecipes(StringBuilder stringBuilder, boolean full) { private void dumpTransformedRecipes(StringBuilder stringBuilder, boolean full) {
result.forEachTransformedRecipe((type, recipes) -> { result.forEachTransformedRecipe((type, recipes) -> {
stringBuilder.append(type.toString()).append(" {\n"); stringBuilder.append(type.toString()).append(" {\n");
recipes.forEach((resourceLocation, entry) -> { recipes.entrySet()
stringBuilder.append("\t- ").append(resourceLocation.toString()).append("\n"); .stream()
if (full) { .sorted(Comparator.comparing(o -> o.getKey().toString()))
stringBuilder.append("\t\t Original: ").append(entry.originalRecipe().toString()).append("\n"); .forEach((e) -> {
stringBuilder stringBuilder.append("\t- ").append(e.getKey().toString()).append("\n");
.append("\t\t Transformed: ") if (full) {
.append(entry.transformedRecipe().toString()) stringBuilder
.append("\n\n"); .append("\t\t Original: ")
} .append(e.getValue().originalRecipe().toString())
}); .append("\n");
stringBuilder
.append("\t\t Transformed: ")
.append(e.getValue().transformedRecipe().toString())
.append("\n\n");
}
});
stringBuilder.append("}\n\n"); stringBuilder.append("}\n\n");
}); });
} }

View file

@ -1,6 +1,6 @@
package com.almostreliable.unified.recipe; package com.almostreliable.unified.recipe;
import com.almostreliable.unified.AlmostUnified; import com.almostreliable.unified.BuildConfig;
import com.almostreliable.unified.utils.JsonCompare; import com.almostreliable.unified.utils.JsonCompare;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -8,14 +8,14 @@ import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.*; import java.util.*;
public class RawRecipe { public class RecipeLink {
private final ResourceLocation id; private final ResourceLocation id;
private final ResourceLocation type; private final ResourceLocation type;
private final JsonObject originalRecipe; private final JsonObject originalRecipe;
@Nullable private DuplicateLink duplicateLink; @Nullable private DuplicateLink duplicateLink;
@Nullable private JsonObject transformedRecipe; @Nullable private JsonObject transformedRecipe;
public RawRecipe(ResourceLocation id, JsonObject originalRecipe) { public RecipeLink(ResourceLocation id, JsonObject originalRecipe) {
this.id = id; this.id = id;
this.originalRecipe = originalRecipe; this.originalRecipe = originalRecipe;
@ -26,6 +26,43 @@ public class RawRecipe {
} }
} }
/**
* Compare two recipes for equality with given rules. Keys from rules will automatically count as ignored field for the base comparison.
* If base comparison succeed then the recipes will be compared for equality with rules from {@link JsonCompare.Rule}.
* Rules are sorted, first rule with the highest priority will be used.
*
* @param first first recipe to compare
* @param second second recipe to compare
* @param rules rules to use for comparison
* @param ignoredFields fields to ignore in comparison
* @return the recipe where rules are applied and the recipes are compared for equality, or null if the recipes are not equal
*/
@Nullable
public static RecipeLink compare(RecipeLink first, RecipeLink second, LinkedHashMap<String, JsonCompare.Rule> rules, List<String> ignoredFields) {
JsonObject selfActual = first.getActual();
JsonObject toCompareActual = second.getActual();
Set<String> ignoredFieldsWithRules = new HashSet<>(first.getIgnoredFields());
ignoredFieldsWithRules.addAll(rules.keySet());
if (JsonCompare.matches(selfActual, toCompareActual, ignoredFieldsWithRules)) {
JsonObject compare = JsonCompare.compare(rules, selfActual, toCompareActual);
if (compare == null) {
return null;
}
if (compare == selfActual) {
return first;
}
if (compare == toCompareActual) {
return second;
}
}
return null;
}
public ResourceLocation getId() { public ResourceLocation getId() {
return id; return id;
} }
@ -38,17 +75,6 @@ public class RawRecipe {
return originalRecipe; return originalRecipe;
} }
private void setDuplicateLink(@Nullable DuplicateLink duplicateLink) {
Objects.requireNonNull(duplicateLink);
if (hasDuplicateLink()) {
throw new IllegalStateException("Recipe already linked");
}
this.duplicateLink = duplicateLink;
this.duplicateLink.addDuplicate(this);
}
public boolean hasDuplicateLink() { public boolean hasDuplicateLink() {
return duplicateLink != null; return duplicateLink != null;
} }
@ -58,13 +84,14 @@ public class RawRecipe {
return duplicateLink; return duplicateLink;
} }
public void setTransformed(JsonObject transformedRecipe) { private void setDuplicateLink(@Nullable DuplicateLink duplicateLink) {
Objects.requireNonNull(transformedRecipe); Objects.requireNonNull(duplicateLink);
if (isTransformed()) { if (hasDuplicateLink()) {
throw new IllegalStateException("Recipe already transformed"); throw new IllegalStateException("Recipe already linked");
} }
this.transformedRecipe = transformedRecipe; this.duplicateLink = duplicateLink;
this.duplicateLink.addDuplicate(this);
} }
@Nullable @Nullable
@ -76,6 +103,14 @@ public class RawRecipe {
return transformedRecipe != null; return transformedRecipe != null;
} }
public void setTransformed(JsonObject transformedRecipe) {
Objects.requireNonNull(transformedRecipe);
if (isTransformed()) {
throw new IllegalStateException("Recipe already transformed");
}
this.transformedRecipe = transformedRecipe;
}
private List<String> getIgnoredFields() { private List<String> getIgnoredFields() {
return List.of("conditions"); return List.of("conditions");
@ -85,33 +120,10 @@ public class RawRecipe {
LinkedHashMap<String, JsonCompare.Rule> rules = new LinkedHashMap<>(); LinkedHashMap<String, JsonCompare.Rule> rules = new LinkedHashMap<>();
rules.put("experience", new JsonCompare.HigherRule()); rules.put("experience", new JsonCompare.HigherRule());
rules.put("cookingtime", new JsonCompare.LowerRule()); rules.put("cookingtime", new JsonCompare.LowerRule());
rules.put("energy", new JsonCompare.HigherRule());
return rules; return rules;
} }
@Nullable
public RawRecipe compare(RawRecipe toCompare) {
JsonObject selfActual = getTransformed() != null ? getTransformed() : originalRecipe;
JsonObject toCompareActual = toCompare.getTransformed() != null ? toCompare.getTransformed()
: toCompare.getOriginal();
if (JsonCompare.matches(selfActual, toCompareActual, getIgnoredFields())) {
JsonObject compare = JsonCompare.compare(getRules(), selfActual, toCompareActual);
if (compare == null) {
return null;
}
if (compare == selfActual) {
return this;
}
if (compare == toCompareActual) {
return toCompare;
}
}
return null;
}
@Override @Override
public String toString() { public String toString() {
String duplicate = duplicateLink != null ? " (duplicate)" : ""; String duplicate = duplicateLink != null ? " (duplicate)" : "";
@ -119,22 +131,29 @@ public class RawRecipe {
return String.format("['%s'] %s%s%s", type, id, duplicate, transformed); return String.format("['%s'] %s%s%s", type, id, duplicate, transformed);
} }
public boolean handleDuplicate(RawRecipe recipe) { /**
* Checks for duplicate against given recipe data. If recipe data already has a duplicate link,
* the master from the link will be used. Otherwise, we will create a new link if needed.
*
* @param recipe Recipe data to check for duplicate against.
* @return True if recipe is a duplicate, false otherwise.
*/
public boolean handleDuplicate(RecipeLink recipe) {
if (hasDuplicateLink()) { if (hasDuplicateLink()) {
throw new IllegalStateException("Recipe already linked"); throw new IllegalStateException("Recipe already linked");
} }
DuplicateLink link = recipe.getDuplicateLink(); DuplicateLink link = recipe.getDuplicateLink();
if(link != null) { if (link != null) {
RawRecipe compare = compare(link.getMaster()); RecipeLink compare = RecipeLink.compare(this, link.getMaster(), getRules(), getIgnoredFields());
if(compare != null) { if (compare != null) {
link.updateMaster(this); link.updateMaster(this);
setDuplicateLink(link); setDuplicateLink(link);
return true; return true;
} }
} else { } else {
RawRecipe compare = compare(recipe); RecipeLink compare = RecipeLink.compare(this, recipe, getRules(), getIgnoredFields());
if(compare != null) { if (compare != null) {
DuplicateLink newLink = new DuplicateLink(compare); DuplicateLink newLink = new DuplicateLink(compare);
setDuplicateLink(newLink); setDuplicateLink(newLink);
recipe.setDuplicateLink(newLink); recipe.setDuplicateLink(newLink);
@ -145,29 +164,33 @@ public class RawRecipe {
return false; return false;
} }
public static class DuplicateLink { public JsonObject getActual() {
private RawRecipe currentMaster; return getTransformed() != null ? getTransformed() : getOriginal();
private final Set<RawRecipe> recipes = new HashSet<>(); }
private DuplicateLink(RawRecipe master) { public static class DuplicateLink {
private final Set<RecipeLink> recipes = new HashSet<>();
private RecipeLink currentMaster;
private DuplicateLink(RecipeLink master) {
updateMaster(master); updateMaster(master);
} }
private void updateMaster(RawRecipe master) { private void updateMaster(RecipeLink master) {
Objects.requireNonNull(master); Objects.requireNonNull(master);
addDuplicate(master); addDuplicate(master);
this.currentMaster = master; this.currentMaster = master;
} }
private void addDuplicate(RawRecipe recipe) { private void addDuplicate(RecipeLink recipe) {
recipes.add(recipe); recipes.add(recipe);
} }
public RawRecipe getMaster() { public RecipeLink getMaster() {
return currentMaster; return currentMaster;
} }
public Set<RawRecipe> getRecipes() { public Set<RecipeLink> getRecipes() {
return Collections.unmodifiableSet(recipes); return Collections.unmodifiableSet(recipes);
} }
@ -175,5 +198,10 @@ public class RawRecipe {
public String toString() { public String toString() {
return "Link{currentMaster=" + currentMaster + ", recipes=" + recipes.size() + "}"; return "Link{currentMaster=" + currentMaster + ", recipes=" + recipes.size() + "}";
} }
public ResourceLocation createNewRecipeId() {
String id = String.format("%s_%s", currentMaster.getId().getNamespace(), currentMaster.getId().getPath());
return new ResourceLocation(BuildConfig.MOD_ID, id);
}
} }
} }

View file

@ -23,17 +23,18 @@ public class RecipeTransformationResult {
this.startTime = System.currentTimeMillis(); this.startTime = System.currentTimeMillis();
} }
public void track(ResourceLocation recipe, JsonObject json, @Nullable JsonObject result) { // TODO refactor or remove and just use RawRecipe
Entry entry = new Entry(json, result); public void track(RecipeLink recipe) {
ResourceLocation type = entry.getType(); Entry entry = new Entry(recipe.getOriginal(), recipe.getTransformed());
ResourceLocation type = recipe.getType();
if (allTrackedRecipes.contains(type, recipe)) { if (allTrackedRecipes.contains(type, recipe.getId())) {
throw new IllegalArgumentException("Already tracking " + type + ":" + recipe); throw new IllegalArgumentException("Already tracking " + type + ":" + recipe);
} }
allTrackedRecipes.put(type, recipe, entry); allTrackedRecipes.put(type, recipe.getId(), entry);
if (entry.isTransformed()) { if (entry.isTransformed()) {
transformedRecipes.put(type, recipe, entry); transformedRecipes.put(type, recipe.getId(), entry);
} }
} }

View file

@ -8,12 +8,10 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable; import java.util.HashSet;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class RecipeTransformer { public class RecipeTransformer {
@ -33,92 +31,87 @@ public class RecipeTransformer {
return false; return false;
} }
/**
* Transforms a map of recipes. This method will modify the map in-place. Part of the transformation is to unify recipes with the given {@link ReplacementMap}.
* After unification, recipes will be checked for duplicates. All duplicates will be removed from the map.
*
* @param recipes The map of recipes to transform.
* @return
*/
public RecipeTransformationResult transformRecipes(Map<ResourceLocation, JsonElement> recipes) { public RecipeTransformationResult transformRecipes(Map<ResourceLocation, JsonElement> recipes) {
ConcurrentMap<ResourceLocation, List<RawRecipe>> rawRecipesByType = recipes AlmostUnified.LOG.warn("Recipe counts: " + recipes.size());
.entrySet() RecipeTransformationResult rtr = new RecipeTransformationResult();
.parallelStream() Map<ResourceLocation, List<RecipeLink>> byType = groupRecipesByType(recipes);
.filter(entry -> entry.getValue().isJsonObject() && hasValidType(entry.getValue().getAsJsonObject())) byType.forEach((type, recipeLinks) -> {
.map(entry -> new RawRecipe(entry.getKey(), entry.getValue().getAsJsonObject())) Set<RecipeLink.DuplicateLink> duplicates = new HashSet<>(recipeLinks.size());
.collect(Collectors.groupingByConcurrent(RawRecipe::getType)); for (RecipeLink curRecipe : recipeLinks) {
transformRecipe(curRecipe);
RecipeTransformationResult recipeTransformationResult = new RecipeTransformationResult(); if (curRecipe.isTransformed()) {
recipes.put(curRecipe.getId(), curRecipe.getTransformed());
Map<ResourceLocation, List<RawRecipe.DuplicateLink>> links = new HashMap<>(); if (handleDuplicate(curRecipe, recipeLinks)) {
rawRecipesByType.forEach((type, rawRecipes) -> { duplicates.add(curRecipe.getDuplicateLink());
for (int curIndex = 0; curIndex < rawRecipes.size(); curIndex++) { }
RawRecipe curRecipe = rawRecipes.get(curIndex);
JsonObject result = transformRecipe(curRecipe.getId(), curRecipe.getOriginal());
if (result != null) {
recipeTransformationResult.track(curRecipe.getId(), curRecipe.getOriginal(), result); // TODO remove
curRecipe.setTransformed(result);
handleDuplicate(curRecipe, rawRecipes);
} }
rtr.track(curRecipe); // TODO remove
} }
// TODO remove later
List<RawRecipe.DuplicateLink> duplicateLinks = rawRecipes for (RecipeLink.DuplicateLink link : duplicates) {
.stream() link.getRecipes().forEach(recipe -> recipes.remove(recipe.getId()));
.map(RawRecipe::getDuplicateLink) recipes.put(link.createNewRecipeId(), link.getMaster().getActual());
.filter(Objects::nonNull)
.distinct()
.toList();
if(duplicateLinks.size() > 0) {
links.put(type, duplicateLinks);
} }
}); });
rtr.end();
recipeTransformationResult.end(); AlmostUnified.LOG.warn("Recipe counts afterwards: " + recipes.size());
return rtr;
// for (var entry : recipes.entrySet()) {
// if (!hasValidType(entry.getValue().getAsJsonObject())) {
// continue;
// }
//
// if (entry.getValue() instanceof JsonObject json) {
// JsonObject result = transformRecipe(entry.getKey(), json);
// recipeTransformationResult.track(entry.getKey(), json, result);
// if (result != null) {
// entry.setValue(result);
// }
// }
// }
return recipeTransformationResult;
} }
private void handleDuplicate(RawRecipe curRecipe, List<RawRecipe> rawRecipes) { private Map<ResourceLocation, List<RecipeLink>> groupRecipesByType(Map<ResourceLocation, JsonElement> recipes) {
if(curRecipe.getDuplicateLink() != null) { return recipes
.entrySet()
.stream()
.filter(entry -> entry.getValue().isJsonObject() && hasValidType(entry.getValue().getAsJsonObject()))
.map(entry -> new RecipeLink(entry.getKey(), entry.getValue().getAsJsonObject()))
.collect(Collectors.groupingByConcurrent(RecipeLink::getType));
}
private boolean handleDuplicate(RecipeLink curRecipe, List<RecipeLink> recipes) {
if (curRecipe.getDuplicateLink() != null) {
AlmostUnified.LOG.error("Duplication already handled for recipe {}", curRecipe.getId()); AlmostUnified.LOG.error("Duplication already handled for recipe {}", curRecipe.getId());
return; return false;
} }
for (RawRecipe rawRecipe : rawRecipes) { for (RecipeLink recipeLink : recipes) {
if (rawRecipe == curRecipe) { if (recipeLink == curRecipe) {
return; continue;
} }
if (curRecipe.handleDuplicate(rawRecipe)) { if (curRecipe.handleDuplicate(recipeLink)) {
return; return true;
} }
} }
return false;
} }
@Nullable /**
public JsonObject transformRecipe(ResourceLocation recipeId, JsonObject json) { * Transforms a single recipe link. This method will modify the recipe link in-place.
* {@link RecipeHandlerFactory} will apply multiple transformations onto the recipe.
* @param recipe The recipe link to transform.
*/
public void transformRecipe(RecipeLink recipe) {
try { try {
RecipeContextImpl ctx = new RecipeContextImpl(json, replacementMap); RecipeContextImpl ctx = new RecipeContextImpl(recipe.getOriginal(), replacementMap);
RecipeTransformationBuilderImpl builder = new RecipeTransformationBuilderImpl(); RecipeTransformationBuilderImpl builder = new RecipeTransformationBuilderImpl();
factory.fillTransformations(builder, ctx); factory.fillTransformations(builder, ctx);
JsonObject result = builder.transform(json, ctx); JsonObject result = builder.transform(recipe.getOriginal(), ctx);
if (result != null) { if (result != null) {
return result; recipe.setTransformed(result);
} }
} catch (Exception e) { } catch (Exception e) {
AlmostUnified.LOG.warn("Error transforming recipe '{}': {}", AlmostUnified.LOG.warn("Error transforming recipe '{}': {}",
recipeId, recipe.getId(),
e.getMessage()); e.getMessage());
e.printStackTrace(); e.printStackTrace();
} }
return null;
} }
} }

View file

@ -1,29 +1,54 @@
package com.almostreliable.unified.utils; package com.almostreliable.unified.utils;
import com.almostreliable.unified.AlmostUnified; import com.almostreliable.unified.AlmostUnified;
import com.almostreliable.unified.api.recipe.ReplacementFallbackStrategy; import com.almostreliable.unified.AlmostUnifiedPlatform;
import com.almostreliable.unified.recipe.fallbacks.StoneStrataFallbackStrategy;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.Predicate;
public class ReplacementMap { public class ReplacementMap {
private final Collection<String> modPriorities; private final Collection<String> modPriorities;
private final List<String> stoneStrata;
private final TagMap tagMap; private final TagMap tagMap;
// TODO - In the future this may be a list of multiple fallbacks.
private final ReplacementFallbackStrategy fallbackStrategy = new StoneStrataFallbackStrategy();
public ReplacementMap(TagMap tagMap, List<String> modPriorities) { public ReplacementMap(TagMap tagMap, List<String> modPriorities, List<String> stoneStrata) {
this.tagMap = tagMap; this.tagMap = tagMap;
this.modPriorities = modPriorities; this.modPriorities = modPriorities;
this.stoneStrata = stoneStrata;
}
/**
* Returns the stone strata from given item. Method works on the requirement that it's an item which has a stone strata.
* Use {@link #isStoneStrataTag(UnifyTag)} to fill this requirement.
*
* @param item The item to get the stone strata from.
* @return The stone strata of the item. Returning empty string means clean-stone strata.
*/
private String getStoneStrata(ResourceLocation item) {
for (String stone : stoneStrata) {
if (item.getPath().startsWith(stone + "_")) {
return stone;
}
}
return "";
}
private boolean isStoneStrataTag(UnifyTag<Item> tag) {
String tagString = tag.location().toString();
return switch (AlmostUnifiedPlatform.INSTANCE.getPlatform()) {
case Forge -> tagString.startsWith("forge:ores/");
case Fabric -> tagString.matches("c:ores/.+") || tagString.matches("c:.+_ore");
};
} }
@Nullable @Nullable
public UnifyTag<Item> getPreferredTag(ResourceLocation item) { public UnifyTag<Item> getPreferredTagForItem(ResourceLocation item) {
Collection<UnifyTag<Item>> tags = tagMap.getTags(item); Collection<UnifyTag<Item>> tags = tagMap.getTags(item);
if (tags.isEmpty()) { if (tags.isEmpty()) {
@ -42,44 +67,31 @@ public class ReplacementMap {
@Nullable @Nullable
public ResourceLocation getReplacementForItem(ResourceLocation item) { public ResourceLocation getReplacementForItem(ResourceLocation item) {
UnifyTag<Item> tag = getPreferredTag(item); UnifyTag<Item> t = getPreferredTagForItem(item);
if (tag == null) { if (t == null) {
return null; return null;
} }
ResourceLocation preferredItem = getPreferredItemByTag(tag, item.getNamespace()); if (isStoneStrataTag(t)) {
if (item.equals(preferredItem)) { String stone = getStoneStrata(item);
return null; return getPreferredItemForTag(t, i -> stone.equals(getStoneStrata(i)));
} }
return preferredItem; return getPreferredItemForTag(t, i -> true);
} }
@Nullable @Nullable
public ResourceLocation getPreferredItemByTag(UnifyTag<Item> tag) { public ResourceLocation getPreferredItemForTag(UnifyTag<Item> tag, Predicate<ResourceLocation> itemFilter) {
return getPreferredItemByTag(tag, null); List<ResourceLocation> items = tagMap
} .getItems(tag)
.stream()
.filter(itemFilter)
.toList();
@Nullable for (String modPriority : modPriorities) {
public ResourceLocation getPreferredItemByTag(UnifyTag<Item> tag, @Nullable String ignoredNamespace) { for (ResourceLocation item : items) {
for (String mod : modPriorities) { if (item.getNamespace().equals(modPriority)) {
if (mod.equals(ignoredNamespace)) { return item;
return null;
}
List<ResourceLocation> sameModItems = tagMap
.getItems(tag)
.stream()
.filter(i -> i.getNamespace().equals(mod))
.toList();
if (sameModItems.size() == 1) {
return sameModItems.get(0);
}
if (sameModItems.size() > 1) {
ResourceLocation fallback = fallbackStrategy.getFallback(tag, sameModItems, tagMap);
if (fallback != null) {
return fallback;
} }
} }
} }

View file

@ -68,7 +68,7 @@ public class TestUtils {
public static RecipeTransformer basicTransformer(Consumer<RecipeHandlerFactory> consumer) { public static RecipeTransformer basicTransformer(Consumer<RecipeHandlerFactory> consumer) {
ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(), ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(),
TestUtils.TEST_MOD_PRIORITIES); TestUtils.TEST_MOD_PRIORITIES, ModConfig.DEFAULT_STONE_STRATA);
RecipeHandlerFactory factory = new RecipeHandlerFactory(); RecipeHandlerFactory factory = new RecipeHandlerFactory();
consumer.accept(factory); consumer.accept(factory);
return new RecipeTransformer(factory, map); return new RecipeTransformer(factory, map);

View file

@ -1,5 +1,6 @@
package com.almostreliable.unified.recipe; package com.almostreliable.unified.recipe;
import com.almostreliable.unified.ModConfig;
import com.almostreliable.unified.TestUtils; import com.almostreliable.unified.TestUtils;
import com.almostreliable.unified.utils.ReplacementMap; import com.almostreliable.unified.utils.ReplacementMap;
import com.almostreliable.unified.utils.TagMapTests; import com.almostreliable.unified.utils.TagMapTests;
@ -7,8 +8,6 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class RecipeContextImplTest { public class RecipeContextImplTest {
public static String mekaTest = """ public static String mekaTest = """
{ {
@ -23,7 +22,7 @@ public class RecipeContextImplTest {
public void depthReplace_MekaTest() { public void depthReplace_MekaTest() {
JsonObject json = new Gson().fromJson(mekaTest, JsonObject.class); JsonObject json = new Gson().fromJson(mekaTest, JsonObject.class);
ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(), ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(),
TestUtils.TEST_MOD_PRIORITIES); TestUtils.TEST_MOD_PRIORITIES, ModConfig.DEFAULT_STONE_STRATA);
// RecipeContextImpl context = new RecipeContextImpl(new ResourceLocation("test"), json, map); // RecipeContextImpl context = new RecipeContextImpl(new ResourceLocation("test"), json, map);
// JsonElement result = context.createResultReplacement(json.getAsJsonObject("output")); // JsonElement result = context.createResultReplacement(json.getAsJsonObject("output"));
// assertNull(result); // assertNull(result);

View file

@ -1,5 +1,6 @@
package com.almostreliable.unified.utils; package com.almostreliable.unified.utils;
import com.almostreliable.unified.ModConfig;
import com.almostreliable.unified.TestUtils; import com.almostreliable.unified.TestUtils;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -14,34 +15,36 @@ public class ReplacementMapTests {
@Test @Test
public void getPreferredItemByTag() { public void getPreferredItemByTag() {
ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(), ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(),
TestUtils.TEST_MOD_PRIORITIES); TestUtils.TEST_MOD_PRIORITIES, ModConfig.DEFAULT_STONE_STRATA);
assertEquals(map.getPreferredItemByTag(TestUtils.BRONZE_ORES_TAG), TestUtils.mod1RL("bronze_ore")); // assertEquals(map.getPreferredItemByTag(TestUtils.BRONZE_ORES_TAG), TestUtils.mod1RL("bronze_ore"));
assertNotEquals(map.getPreferredItemByTag(TestUtils.BRONZE_ORES_TAG), TestUtils.mod2RL("bronze_ore")); // assertNotEquals(map.getPreferredItemByTag(TestUtils.BRONZE_ORES_TAG), TestUtils.mod2RL("bronze_ore"));
assertEquals(map.getPreferredItemByTag(TestUtils.INVAR_ORES_TAG), TestUtils.mod1RL("invar_ore")); // assertEquals(map.getPreferredItemByTag(TestUtils.INVAR_ORES_TAG), TestUtils.mod1RL("invar_ore"));
assertNotEquals(map.getPreferredItemByTag(TestUtils.INVAR_ORES_TAG), TestUtils.mod2RL("invar_ore")); // assertNotEquals(map.getPreferredItemByTag(TestUtils.INVAR_ORES_TAG), TestUtils.mod2RL("invar_ore"));
assertEquals(map.getPreferredItemByTag(TestUtils.TIN_ORES_TAG), TestUtils.mod3RL("tin_ore")); // assertEquals(map.getPreferredItemByTag(TestUtils.TIN_ORES_TAG), TestUtils.mod3RL("tin_ore"));
assertNotEquals(map.getPreferredItemByTag(TestUtils.TIN_ORES_TAG), TestUtils.mod4RL("tin_ore")); // assertNotEquals(map.getPreferredItemByTag(TestUtils.TIN_ORES_TAG), TestUtils.mod4RL("tin_ore"));
assertEquals(map.getPreferredItemByTag(TestUtils.SILVER_ORES_TAG), TestUtils.mod3RL("silver_ore")); // assertEquals(map.getPreferredItemByTag(TestUtils.SILVER_ORES_TAG), TestUtils.mod3RL("silver_ore"));
assertNotEquals(map.getPreferredItemByTag(TestUtils.SILVER_ORES_TAG), TestUtils.mod4RL("silver_ore")); // assertNotEquals(map.getPreferredItemByTag(TestUtils.SILVER_ORES_TAG), TestUtils.mod4RL("silver_ore"));
} }
@Test @Test
public void getPreferredItemByTag_ReversePriority() { public void getPreferredItemByTag_ReversePriority() {
// We reverse the order. See `testTagMap` for the mapping. // We reverse the order. See `testTagMap` for the mapping.
List<String> reverse = Lists.reverse(TestUtils.TEST_MOD_PRIORITIES); List<String> reverse = Lists.reverse(TestUtils.TEST_MOD_PRIORITIES);
ReplacementMap reverseMap = new ReplacementMap(TagMapTests.testTagMap(), reverse); ReplacementMap reverseMap = new ReplacementMap(TagMapTests.testTagMap(),
assertEquals(reverseMap.getPreferredItemByTag(TestUtils.BRONZE_ORES_TAG), TestUtils.mod3RL("bronze_ore")); reverse,
assertEquals(reverseMap.getPreferredItemByTag(TestUtils.INVAR_ORES_TAG), TestUtils.mod4RL("invar_ore")); ModConfig.DEFAULT_STONE_STRATA);
assertEquals(reverseMap.getPreferredItemByTag(TestUtils.TIN_ORES_TAG), TestUtils.mod4RL("tin_ore")); // assertEquals(reverseMap.getPreferredItemByTag(TestUtils.BRONZE_ORES_TAG), TestUtils.mod3RL("bronze_ore"));
assertEquals(reverseMap.getPreferredItemByTag(TestUtils.SILVER_ORES_TAG), TestUtils.mod5RL("silver_ore")); // assertEquals(reverseMap.getPreferredItemByTag(TestUtils.INVAR_ORES_TAG), TestUtils.mod4RL("invar_ore"));
// assertEquals(reverseMap.getPreferredItemByTag(TestUtils.TIN_ORES_TAG), TestUtils.mod4RL("tin_ore"));
// assertEquals(reverseMap.getPreferredItemByTag(TestUtils.SILVER_ORES_TAG), TestUtils.mod5RL("silver_ore"));
} }
@Test @Test
public void getPreferredTag() { public void getPreferredTag() {
ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(), ReplacementMap map = new ReplacementMap(TagMapTests.testTagMap(),
TestUtils.TEST_MOD_PRIORITIES); TestUtils.TEST_MOD_PRIORITIES, ModConfig.DEFAULT_STONE_STRATA);
assertEquals(map.getPreferredTag(TestUtils.mod1RL("bronze_ore")), TestUtils.BRONZE_ORES_TAG); assertEquals(map.getPreferredTagForItem(TestUtils.mod1RL("bronze_ore")), TestUtils.BRONZE_ORES_TAG);
assertNull(map.getPreferredTag(new ResourceLocation("minecraft:diamond")), "We don't have a tag for diamond"); assertNull(map.getPreferredTagForItem(new ResourceLocation("minecraft:diamond")), "We don't have a tag for diamond");
} }
} }

View file

@ -8,8 +8,8 @@ import java.nio.file.Path;
public class AlmostUnifiedPlatformFabric implements AlmostUnifiedPlatform { public class AlmostUnifiedPlatformFabric implements AlmostUnifiedPlatform {
@Override @Override
public String getPlatformName() { public Platform getPlatform() {
return "Fabric"; return Platform.Fabric;
} }
@Override @Override

View file

@ -12,8 +12,8 @@ import java.nio.file.Path;
public class AlmostUnifiedPlatformForge implements AlmostUnifiedPlatform { public class AlmostUnifiedPlatformForge implements AlmostUnifiedPlatform {
@Override @Override
public String getPlatformName() { public Platform getPlatform() {
return "Forge"; return Platform.Forge;
} }
@Override @Override

View file

@ -53,7 +53,7 @@ public class IERecipeHandler implements RecipeHandler {
*/ */
ResourceLocation item = context.getPreferredItemByTag(Utils.toItemTag(object ResourceLocation item = context.getPreferredItemByTag(Utils.toItemTag(object
.get(RecipeConstants.TAG) .get(RecipeConstants.TAG)
.getAsString())); .getAsString()), $ -> true);
if (item != null) { if (item != null) {
object.remove(RecipeConstants.TAG); object.remove(RecipeConstants.TAG);
object.addProperty(RecipeConstants.ITEM, item.toString()); object.addProperty(RecipeConstants.ITEM, item.toString());

View file

@ -2,6 +2,7 @@ package com.almostreliable.unified.compat.ie;
import com.almostreliable.unified.TestUtils; import com.almostreliable.unified.TestUtils;
import com.almostreliable.unified.api.ModConstants; import com.almostreliable.unified.api.ModConstants;
import com.almostreliable.unified.recipe.RecipeLink;
import com.almostreliable.unified.recipe.RecipeTransformer; import com.almostreliable.unified.recipe.RecipeTransformer;
import com.almostreliable.unified.utils.JsonQuery; import com.almostreliable.unified.utils.JsonQuery;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -30,8 +31,9 @@ public class IERecipeHandlerTest {
RecipeTransformer transformer = TestUtils.basicTransformer(f -> f.registerForMod(ModConstants.IE, RecipeTransformer transformer = TestUtils.basicTransformer(f -> f.registerForMod(ModConstants.IE,
new IERecipeHandler())); new IERecipeHandler()));
JsonObject alloy = gson.fromJson(simpleAlloyRecipe, JsonObject.class); JsonObject alloy = gson.fromJson(simpleAlloyRecipe, JsonObject.class);
JsonObject result = transformer.transformRecipe(defaultRecipeId, alloy); RecipeLink recipe = new RecipeLink(defaultRecipeId, alloy);
assertNull(result, "Nothing to transform, so it should be null"); transformer.transformRecipe(recipe);
assertFalse(recipe.isTransformed(), "Nothing to transform, so it should be false");
} }
@Test @Test
@ -43,11 +45,13 @@ public class IERecipeHandlerTest {
.getAsJsonObject("result") .getAsJsonObject("result")
.getAsJsonObject("base_ingredient") .getAsJsonObject("base_ingredient")
.addProperty("tag", TestUtils.BRONZE_ORES_TAG.location().toString()); .addProperty("tag", TestUtils.BRONZE_ORES_TAG.location().toString());
JsonObject result = transformer.transformRecipe(defaultRecipeId, alloy); RecipeLink recipe = new RecipeLink(defaultRecipeId, alloy);
assertNotEquals(result, alloy, "Result should be different"); transformer.transformRecipe(recipe);
assertNotNull(result, "Result should not be null");
assertNull(JsonQuery.of(result, "result/base_ingredient/tag"), "Tag key should be removed"); assertNotEquals(recipe.getTransformed(), alloy, "Result should be different");
assertEquals(JsonQuery.of(result, "result/base_ingredient/item").asString(), assertNotNull(recipe.getTransformed(), "Result should not be null");
assertNull(JsonQuery.of(recipe.getTransformed(), "result/base_ingredient/tag"), "Tag key should be removed");
assertEquals(JsonQuery.of(recipe.getTransformed(), "result/base_ingredient/item").asString(),
TestUtils.mod1RL("bronze_ore").toString(), TestUtils.mod1RL("bronze_ore").toString(),
"Result should be bronze_ore"); "Result should be bronze_ore");
} }
@ -62,10 +66,12 @@ public class IERecipeHandlerTest {
.getAsJsonObject("result") .getAsJsonObject("result")
.getAsJsonObject("base_ingredient") .getAsJsonObject("base_ingredient")
.addProperty("item", TestUtils.mod3RL("bronze_ore").toString()); .addProperty("item", TestUtils.mod3RL("bronze_ore").toString());
JsonObject result = transformer.transformRecipe(defaultRecipeId, alloy); RecipeLink recipe = new RecipeLink(defaultRecipeId, alloy);
assertNotEquals(result, alloy, "Result should be different"); transformer.transformRecipe(recipe);
assertNotNull(result, "Result should not be null");
assertEquals(JsonQuery.of(result, ("result/base_ingredient/item")).asString(), assertNotEquals(recipe.getTransformed(), alloy, "Result should be different");
assertNotNull(recipe.getTransformed(), "Result should not be null");
assertEquals(JsonQuery.of(recipe.getTransformed(), ("result/base_ingredient/item")).asString(),
TestUtils.mod1RL("bronze_ore").toString(), TestUtils.mod1RL("bronze_ore").toString(),
"Transformer should replace bronze_ore from mod3 with bronze_ore from mod1"); "Transformer should replace bronze_ore from mod3 with bronze_ore from mod1");
} }
@ -80,10 +86,12 @@ public class IERecipeHandlerTest {
.getAsJsonObject("result") .getAsJsonObject("result")
.getAsJsonObject("base_ingredient") .getAsJsonObject("base_ingredient")
.addProperty("item", TestUtils.mod3RL("bronze_ore").toString()); .addProperty("item", TestUtils.mod3RL("bronze_ore").toString());
JsonObject result = transformer.transformRecipe(defaultRecipeId, alloy); RecipeLink recipe = new RecipeLink(defaultRecipeId, alloy);
assertNotEquals(result, alloy, "Result should be different"); transformer.transformRecipe(recipe);
assertNotNull(result, "Result should not be null");
assertEquals(JsonQuery.of(result, ("result/base_ingredient/item")).asString(), assertNotEquals(recipe.getTransformed(), alloy, "Result should be different");
assertNotNull(recipe.getTransformed(), "Result should not be null");
assertEquals(JsonQuery.of(recipe.getTransformed(), ("result/base_ingredient/item")).asString(),
TestUtils.mod1RL("bronze_ore").toString(), TestUtils.mod1RL("bronze_ore").toString(),
"Transformer should replace bronze_ore from mod3 with bronze_ore from mod1"); "Transformer should replace bronze_ore from mod3 with bronze_ore from mod1");
} }