From 2671febbe26b9cf3a310f77ab72794c9fd02e6dc Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:36:00 +0200 Subject: [PATCH 1/3] feat: record all failed-forge inputs on scrap --- README.md | 2 + .../objects/alloys/AlloyForger.java | 5 +++ .../objects/data/ScrapProvenance.java | 34 ++++++++++++++++ .../advancedcrafting/utils/PDCKeys.java | 4 ++ .../advancedcrafting/ForgerCoverageTest.java | 2 + .../advancedcrafting/ScrapInputsTest.java | 40 +++++++++++++++++++ 6 files changed, 87 insertions(+) create mode 100644 src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java diff --git a/README.md b/README.md index 1e70d5d..73bc6f4 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,5 @@ runs when a report was generated. Coverage proves the exercised Java behavior. It does not replace a live Paper server integration check with the pinned ItemsAdder/MMOItems/TLibs versions. + +Failed alloy forges record consumed ingredient quantities on scrap for Recycler recovery. Older scrap retains only its recorded base; missing catalyst history cannot be recovered. diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java index 19539e1..d0aeb29 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java @@ -96,6 +96,11 @@ public NamableAlloy forge(Player p, int i) { ItemManager itemManager = MMOItems.plugin.getItems(); ItemStack template = itemManager.getMMOItem(MMOItems.plugin.getTypes().get(scrapType),scrapId).newBuilder().build(); if(station.getBaseItem() != null) ScrapProvenance.applyTo(template, station.getBaseItem().getId()); + var consumed = new java.util.LinkedHashMap(); + for (Ingredient ingredient : station.getIngredients()) { + consumed.merge(ingredient.getId(), 1, Integer::sum); + } + ScrapProvenance.applyInputs(template, consumed); loc.getWorld().dropItem(loc, template); return null; } diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java index 21f9426..3127153 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java @@ -1,12 +1,17 @@ package net.tfminecraft.advancedcrafting.objects.data; import java.util.Locale; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; import net.tfminecraft.advancedcrafting.utils.PDCKeys; +import net.tfminecraft.advancedcrafting.AdvancedCrafting; /** * Records which base ingredient a failed alloy forge consumed, so scrap can be recycled into it. @@ -34,4 +39,33 @@ public static String readBaseId(ItemStack item) { } return item.getItemMeta().getPersistentDataContainer().get(PDCKeys.scrapBase(), PersistentDataType.STRING); } + + /** Records consumed ingredient quantities per scrap, independently of the legacy base tag. */ + public static void applyInputs(ItemStack item, Map amounts) { + ItemMeta meta = item.getItemMeta(); + var container = meta.getPersistentDataContainer(); + var inputs = container.getAdapterContext().newPersistentDataContainer(); + amounts.forEach((id, amount) -> inputs.set( + new NamespacedKey(AdvancedCrafting.plugin, id.toLowerCase(Locale.ROOT)), + PersistentDataType.INTEGER, amount)); + container.set(PDCKeys.scrapInputs(), PersistentDataType.TAG_CONTAINER, inputs); + item.setItemMeta(meta); + } + + /** Legacy scrap contains only a base tag; its catalysts cannot be reconstructed. */ + public static Map readInputs(ItemStack item) { + if (item == null || !item.hasItemMeta()) return Map.of(); + var inputs = item.getItemMeta().getPersistentDataContainer().get( + PDCKeys.scrapInputs(), PersistentDataType.TAG_CONTAINER); + if (inputs == null) { + String base = readBaseId(item); + return base == null || base.isBlank() ? Map.of() : Map.of(base, 1); + } + Map amounts = new LinkedHashMap<>(); + for (var key : inputs.getKeys()) { + Integer amount = inputs.get(key, PersistentDataType.INTEGER); + if (amount != null && amount > 0) amounts.put(key.getKey(), amount); + } + return amounts; + } } diff --git a/src/main/java/net/tfminecraft/advancedcrafting/utils/PDCKeys.java b/src/main/java/net/tfminecraft/advancedcrafting/utils/PDCKeys.java index 5ce75ff..c737a7e 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/utils/PDCKeys.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/utils/PDCKeys.java @@ -28,6 +28,10 @@ public static NamespacedKey scrapBase() { return new NamespacedKey(AdvancedCrafting.plugin, "ac_scrap_base"); } + public static NamespacedKey scrapInputs() { + return new NamespacedKey(AdvancedCrafting.plugin, "ac_scrap_inputs"); + } + public static NamespacedKey ingredientId() { return new NamespacedKey(AdvancedCrafting.plugin, "ac_ingredient_id"); } diff --git a/src/test/java/net/tfminecraft/advancedcrafting/ForgerCoverageTest.java b/src/test/java/net/tfminecraft/advancedcrafting/ForgerCoverageTest.java index 06725a1..3806524 100644 --- a/src/test/java/net/tfminecraft/advancedcrafting/ForgerCoverageTest.java +++ b/src/test/java/net/tfminecraft/advancedcrafting/ForgerCoverageTest.java @@ -78,6 +78,8 @@ void forgeReturnsScrapKnownAlloysNewDiscoveriesAndRecoversStaleIndexes() throws try (var dbs = mockConstruction(AlloyDatabase.class)) { assertNull(forger(station, 0).forge(p)); verify(dbs.constructed().getFirst()).saveRecipe(station, "scrap"); + var dropped = loc.getWorld().getEntitiesByClass(org.bukkit.entity.Item.class).iterator().next().getItemStack(); + assertEquals(Map.of("iron", 1, "copper", 1), ScrapProvenance.readInputs(dropped)); } try (var dbs = mockConstruction( diff --git a/src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java b/src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java new file mode 100644 index 0000000..98aad9c --- /dev/null +++ b/src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java @@ -0,0 +1,40 @@ +package net.tfminecraft.advancedcrafting; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.Map; +import net.tfminecraft.advancedcrafting.objects.data.ScrapProvenance; +import net.tfminecraft.advancedcrafting.utils.PDCKeys; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.junit.jupiter.api.Test; + +class ScrapInputsTest extends CoverageSupport { + @Test + void inputsRoundTripQuantitiesAndLegacyScrapReturnsOnlyItsBase() { + var scrap = new ItemStack(Material.IRON_NUGGET); + assertTrue(ScrapProvenance.readInputs(null).isEmpty()); + assertTrue(ScrapProvenance.readInputs(scrap).isEmpty()); + scrap.setItemMeta(scrap.getItemMeta()); + var meta = scrap.getItemMeta(); + meta.setDisplayName("Scrap"); + scrap.setItemMeta(meta); + assertTrue(ScrapProvenance.readInputs(scrap).isEmpty()); + ScrapProvenance.applyTo(scrap, "IRON"); + assertEquals(Map.of("iron", 1), ScrapProvenance.readInputs(scrap)); + ScrapProvenance.applyInputs(scrap, Map.of("IRON", 2, "ruby", 3, "zero", 0)); + assertEquals(Map.of("iron", 2, "ruby", 3), ScrapProvenance.readInputs(scrap)); + meta = scrap.getItemMeta(); + var inputs = meta.getPersistentDataContainer().get(PDCKeys.scrapInputs(), PersistentDataType.TAG_CONTAINER); + inputs.set(new NamespacedKey(plugin, "bad"), PersistentDataType.STRING, "bad"); + meta.getPersistentDataContainer().set(PDCKeys.scrapInputs(), PersistentDataType.TAG_CONTAINER, inputs); + scrap.setItemMeta(meta); + assertEquals(Map.of("iron", 2, "ruby", 3), ScrapProvenance.readInputs(scrap)); + meta = scrap.getItemMeta(); + meta.getPersistentDataContainer().remove(PDCKeys.scrapInputs()); + meta.getPersistentDataContainer().set(PDCKeys.scrapBase(), PersistentDataType.STRING, " "); + scrap.setItemMeta(meta); + assertTrue(ScrapProvenance.readInputs(scrap).isEmpty()); + } +} From 520eaa459e9ef0f8306468845617c854b1779203 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:39:54 +0200 Subject: [PATCH 2/3] test: cover scrap without item metadata --- .../java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java b/src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java index 98aad9c..c441401 100644 --- a/src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java +++ b/src/test/java/net/tfminecraft/advancedcrafting/ScrapInputsTest.java @@ -15,6 +15,7 @@ class ScrapInputsTest extends CoverageSupport { void inputsRoundTripQuantitiesAndLegacyScrapReturnsOnlyItsBase() { var scrap = new ItemStack(Material.IRON_NUGGET); assertTrue(ScrapProvenance.readInputs(null).isEmpty()); + assertTrue(ScrapProvenance.readInputs(org.mockito.Mockito.mock(ItemStack.class)).isEmpty()); assertTrue(ScrapProvenance.readInputs(scrap).isEmpty()); scrap.setItemMeta(scrap.getItemMeta()); var meta = scrap.getItemMeta(); From 1fde3c6c6e222a372198c1c307be2bdc66663f52 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:53:25 +0200 Subject: [PATCH 3/3] fix: skip scrap quantities stored with the wrong type --- .../advancedcrafting/objects/data/ScrapProvenance.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java index 3127153..8fe2f21 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/data/ScrapProvenance.java @@ -63,8 +63,9 @@ public static Map readInputs(ItemStack item) { } Map amounts = new LinkedHashMap<>(); for (var key : inputs.getKeys()) { - Integer amount = inputs.get(key, PersistentDataType.INTEGER); - if (amount != null && amount > 0) amounts.put(key.getKey(), amount); + if (!inputs.has(key, PersistentDataType.INTEGER)) continue; + int amount = inputs.get(key, PersistentDataType.INTEGER); + if (amount > 0) amounts.put(key.getKey(), amount); } return amounts; }