diff --git a/Common/src/main/java/com/almostreliable/unified/recipe/RawRecipe.java b/Common/src/main/java/com/almostreliable/unified/recipe/RawRecipe.java new file mode 100644 index 0000000..3c1b859 --- /dev/null +++ b/Common/src/main/java/com/almostreliable/unified/recipe/RawRecipe.java @@ -0,0 +1,65 @@ +package com.almostreliable.unified.recipe; + +import com.google.gson.JsonObject; +import net.minecraft.resources.ResourceLocation; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class RawRecipe { + private final ResourceLocation id; + private final ResourceLocation type; + private final JsonObject originalRecipe; + private boolean isDuplicate = false; + @Nullable private JsonObject transformedRecipe; + + + public RawRecipe(ResourceLocation id, JsonObject originalRecipe) { + this.id = id; + this.originalRecipe = originalRecipe; + + try { + this.type = ResourceLocation.tryParse(originalRecipe.get("type").getAsString()); + } catch (Exception e) { + throw new IllegalArgumentException("Could not detect recipe type"); + } + } + + public ResourceLocation getId() { + return id; + } + + public ResourceLocation getType() { + return type; + } + + public JsonObject getOriginal() { + return originalRecipe; + } + + public void markAsDuplicate() { + isDuplicate = true; + } + + public boolean isDuplicate() { + return isDuplicate; + } + + public void setTransformed(JsonObject transformedRecipe) { + Objects.requireNonNull(transformedRecipe); + if(isTransformed()) { + throw new IllegalStateException("Recipe already transformed"); + } + + this.transformedRecipe = transformedRecipe; + } + + @Nullable + public JsonObject getTransformed() { + return transformedRecipe; + } + + public boolean isTransformed() { + return transformedRecipe != null; + } +} diff --git a/Common/src/main/java/com/almostreliable/unified/recipe/RecipeTransformer.java b/Common/src/main/java/com/almostreliable/unified/recipe/RecipeTransformer.java index 4ce3d40..e0e1bb7 100644 --- a/Common/src/main/java/com/almostreliable/unified/recipe/RecipeTransformer.java +++ b/Common/src/main/java/com/almostreliable/unified/recipe/RecipeTransformer.java @@ -9,7 +9,11 @@ import com.google.gson.JsonPrimitive; import net.minecraft.resources.ResourceLocation; import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; public class RecipeTransformer { @@ -29,23 +33,59 @@ public class RecipeTransformer { } public RecipeTransformationResult transformRecipes(Map recipes) { + ConcurrentMap> rawRecipesByType = recipes + .entrySet() + .parallelStream() + .filter(entry -> entry.getValue().isJsonObject() && hasValidType(entry.getValue().getAsJsonObject())) + .map(entry -> new RawRecipe(entry.getKey(), entry.getValue().getAsJsonObject())) + .collect(Collectors.groupingByConcurrent(RawRecipe::getType)); + RecipeTransformationResult recipeTransformationResult = new RecipeTransformationResult(); - 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); + rawRecipesByType.forEach((type, rawRecipes) -> { + for (int curIndex = 0; curIndex < rawRecipes.size(); curIndex++) { + RawRecipe curRecipe = rawRecipes.get(curIndex); + JsonObject result = transformRecipe(curRecipe.getId(), curRecipe.getOriginal()); if (result != null) { - entry.setValue(result); + recipeTransformationResult.track(curRecipe.getId(), curRecipe.getOriginal(), result); // TODO remove + + curRecipe.setTransformed(result); + + for (int compareIndex = 0; compareIndex < curIndex; compareIndex++) { // TODO extract + RawRecipe toCompare = rawRecipes.get(compareIndex); + if(toCompare.isDuplicate()) continue; + JsonObject json = toCompare.isTransformed() ? toCompare.getTransformed() : toCompare.getOriginal(); + if(result.equals(json)) { // TODO replace with cool equal which has additional compare rules + toCompare.markAsDuplicate(); + } + } } } - } + }); + + Map> duplicates = new HashMap<>(); + rawRecipesByType.forEach((type, rawRecipes) -> { + List duplicatesForType = rawRecipes.stream().filter(RawRecipe::isDuplicate).collect(Collectors.toList()); + if(!duplicatesForType.isEmpty()) { + duplicates.put(type, duplicatesForType); + } + }); recipeTransformationResult.end(); + +// 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; } diff --git a/Common/src/main/java/com/almostreliable/unified/utils/JsonCompare.java b/Common/src/main/java/com/almostreliable/unified/utils/JsonCompare.java new file mode 100644 index 0000000..4696ed5 --- /dev/null +++ b/Common/src/main/java/com/almostreliable/unified/utils/JsonCompare.java @@ -0,0 +1,87 @@ +package com.almostreliable.unified.utils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; + +public class JsonCompare { + + public static int compare(JsonObject first, JsonObject second, LinkedHashMap rules) { + for (var entry : rules.entrySet()) { + JsonElement fElement = first.get(entry.getKey()); + JsonElement sElement = second.get(entry.getKey()); + + if (fElement == null && sElement == null) { + continue; + } + + int compareIndex = entry.getValue().compare(fElement, sElement); + if (compareIndex != 0) { + return compareIndex; + } + } + return 0; + } + + public static JsonObject compare(LinkedHashMap rules, JsonObject... jsonObjects) { + List unsorted = Arrays.asList(jsonObjects); + unsorted.sort((f, s) -> compare(f, s, rules)); + return unsorted.get(0); + } + + public static boolean matches(JsonObject first, JsonObject second, Collection ignoredProperties) { + List firstValidKeys = first + .keySet() + .stream() + .filter(key -> !ignoredProperties.contains(key)) + .toList(); + List secondValidKeys = second + .keySet() + .stream() + .filter(key -> !ignoredProperties.contains(key)) + .toList(); + + if (firstValidKeys.size() != secondValidKeys.size()) return false; + + for (String firstKey : firstValidKeys) { + if (!first.get(firstKey).equals(second.get(firstKey))) { + return false; + } + } + + return true; + } + + @FunctionalInterface + public interface Rule { + /** + * Compare two JsonElements. The caller must ensure that at least one element is not null. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + int compare(@Nullable JsonElement first, @Nullable JsonElement second); + } + + public static class LowerRule implements Rule { + @Override + public int compare(@Nullable JsonElement first, @Nullable JsonElement second) { + double firstValue = first instanceof JsonPrimitive fp ? fp.getAsDouble() : 0; + double secondValue = second instanceof JsonPrimitive sp ? sp.getAsDouble() : 0; + return Double.compare(firstValue, secondValue); + } + } + + public static class HigherRule implements Rule { + @Override + public int compare(@Nullable JsonElement first, @Nullable JsonElement second) { + double firstValue = first instanceof JsonPrimitive fp ? fp.getAsDouble() : 0; + double secondValue = second instanceof JsonPrimitive sp ? sp.getAsDouble() : 0; + return Double.compare(secondValue, firstValue); + } + } +} diff --git a/Common/src/test/java/com/almostreliable/unified/TestUtils.java b/Common/src/test/java/com/almostreliable/unified/TestUtils.java index b55be58..1685c8e 100644 --- a/Common/src/test/java/com/almostreliable/unified/TestUtils.java +++ b/Common/src/test/java/com/almostreliable/unified/TestUtils.java @@ -5,6 +5,8 @@ import com.almostreliable.unified.recipe.handler.RecipeHandlerFactory; import com.almostreliable.unified.utils.ReplacementMap; import com.almostreliable.unified.utils.TagMapTests; import com.almostreliable.unified.utils.UnifyTag; +import com.google.gson.Gson; +import com.google.gson.JsonObject; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -71,4 +73,15 @@ public class TestUtils { consumer.accept(factory); return new RecipeTransformer(factory, map); } + + public static JsonObject json(String json) { + return new Gson().fromJson(json, JsonObject.class); + } + + public static JsonObject json(String json, Consumer consumer) { + Gson gson = new Gson(); + JsonObject obj = gson.fromJson(json, JsonObject.class); + consumer.accept(obj); + return obj; + } } diff --git a/Common/src/test/java/com/almostreliable/unified/util/JsonCompareTest.java b/Common/src/test/java/com/almostreliable/unified/util/JsonCompareTest.java new file mode 100644 index 0000000..a626c98 --- /dev/null +++ b/Common/src/test/java/com/almostreliable/unified/util/JsonCompareTest.java @@ -0,0 +1,115 @@ +package com.almostreliable.unified.util; + +import com.almostreliable.unified.TestUtils; +import com.almostreliable.unified.utils.JsonCompare; +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class JsonCompareTest { + public static String recipe = """ + { + "type": "minecraft:smelting", + "group": "coal", + "ingredient": { + "item": "minecraft:coal_ore" + }, + "result": "minecraft:coal", + "experience": 0.1, + "cookingtime": 200 + } + """; + + @Test + public void simpleCompareFirst() { + JsonObject first = TestUtils.json(recipe, j -> j.addProperty("experience", 0.2)); + JsonObject second = TestUtils.json(recipe); // 0.1 experience + + LinkedHashMap rules = new LinkedHashMap<>(); + rules.put("experience", new JsonCompare.LowerRule()); + JsonObject result = JsonCompare.compare(rules, first, second); + assertEquals(second, result); + } + + @Test + public void simpleCompareSecond() { + JsonObject first = TestUtils.json(recipe, j -> j.addProperty("experience", 0.05)); + JsonObject second = TestUtils.json(recipe); // 0.1 experience + + LinkedHashMap rules = new LinkedHashMap<>(); + rules.put("experience", new JsonCompare.LowerRule()); + JsonObject result = JsonCompare.compare(rules, first, second); + assertEquals(first, result); + } + + @Test + public void compareHigherWins() { + JsonObject first = TestUtils.json(recipe, j -> j.addProperty("experience", 0.05)); + JsonObject second = TestUtils.json(recipe); // 0.1 experience // 0.1 experience + + LinkedHashMap rules = new LinkedHashMap<>(); + rules.put("experience", new JsonCompare.HigherRule()); + JsonObject result = JsonCompare.compare(rules, first, second); + assertEquals(second, result); + } + + @Test + public void compareMulti() { + JsonObject a = TestUtils.json(recipe, j -> { + j.addProperty("experience", 0.1); + j.addProperty("cookingtime", 100); + }); + JsonObject b = TestUtils.json(recipe, j -> j.addProperty("experience", 0.1)); + JsonObject c = TestUtils.json(recipe, j -> { + j.addProperty("experience", 0.1); + j.addProperty("cookingtime", 50); + }); + JsonObject d = TestUtils.json(recipe, j -> j.addProperty("experience", 0.2)); + JsonObject e = TestUtils.json(recipe, j -> j.addProperty("experience", 0.2)); + JsonObject f = TestUtils.json(recipe, j -> j.addProperty("experience", 0.1)); + JsonObject g = TestUtils.json(recipe, j -> { + j.addProperty("experience", 0.2); + j.addProperty("cookingtime", 100); + }); + + LinkedHashMap rules = new LinkedHashMap<>(); + rules.put("experience", new JsonCompare.HigherRule()); + rules.put("cookingtime", new JsonCompare.LowerRule()); + + List list = Arrays.asList(a, b, c, d, e, f, g); + list.sort((first, second) -> JsonCompare.compare(first, second, rules)); + List results = Arrays.asList(g, d, e, c, a, b, f); + for (int i = 0; i < list.size(); i++) { + assertEquals(results.get(i), list.get(i), "Failed at index " + i); + } + } + + @Test + public void simpleMatch() { + JsonObject first = TestUtils.json(recipe); + JsonObject second = TestUtils.json(recipe); + boolean matches = JsonCompare.matches(first, second, List.of()); + assertTrue(matches); + } + + @Test + public void noMatch() { + JsonObject first = TestUtils.json(recipe, j -> j.addProperty("experience", 100)); + JsonObject second = TestUtils.json(recipe); + boolean matches = JsonCompare.matches(first, second, List.of()); + assertFalse(matches); + } + + @Test + public void matchBecauseIgnore() { + JsonObject first = TestUtils.json(recipe, j -> j.addProperty("experience", 100)); + JsonObject second = TestUtils.json(recipe); + boolean matches = JsonCompare.matches(first, second, List.of("experience")); + assertTrue(matches); + } +}