From a3242d4453aa185d2c1508b87bc28547aa2eb3da Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:50:58 +0200 Subject: [PATCH 1/2] feat: record the materials each mage weapon was crafted from The weapon now carries what the craft actually charged (gear_craft_inputs PDC, item path to amount; empty for staff bypass crafts). Socket rewrites and refreshes copy it along with the part list. Recycler reads it with GearProvenance.readInputs instead of today's part costs. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../magic/gear/GearItemBuilder.java | 6 +++ .../net/tfminecraft/magic/gear/GearKeys.java | 5 +++ .../magic/gear/GearProvenance.java | 44 +++++++++++++++++++ .../tfminecraft/magic/gear/GearRefresher.java | 1 + .../magic/gear/gui/GearInventoryManager.java | 2 + 5 files changed, 58 insertions(+) diff --git a/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java b/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java index 3358033..7c23500 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java @@ -147,6 +147,12 @@ private static void copyGearPdc(ItemStack from, ItemStack to) { toMeta.getPersistentDataContainer().set( GearKeys.parts(), org.bukkit.persistence.PersistentDataType.STRING, parts); } + String craftInputs = fromMeta.getPersistentDataContainer().get( + GearKeys.craftInputs(), org.bukkit.persistence.PersistentDataType.STRING); + if (craftInputs != null) { + toMeta.getPersistentDataContainer().set( + GearKeys.craftInputs(), org.bukkit.persistence.PersistentDataType.STRING, craftInputs); + } toMeta.getPersistentDataContainer().set( GearKeys.archetype(), org.bukkit.persistence.PersistentDataType.STRING, diff --git a/src/main/java/net/tfminecraft/magic/gear/GearKeys.java b/src/main/java/net/tfminecraft/magic/gear/GearKeys.java index 8ba4087..71bc28c 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearKeys.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearKeys.java @@ -24,6 +24,11 @@ public static NamespacedKey parts() { return new NamespacedKey(Magic.plugin, "gear_parts"); } + /** Materials actually charged when the weapon was crafted, as a JSON map of item path to amount. */ + public static NamespacedKey craftInputs() { + return new NamespacedKey(Magic.plugin, "gear_craft_inputs"); + } + public static NamespacedKey socketsLocked() { return new NamespacedKey(Magic.plugin, "gear_sockets_locked"); } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java b/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java index 7bfdfdf..0f163fb 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java @@ -1,16 +1,26 @@ package net.tfminecraft.magic.gear; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + public final class GearProvenance { + private static final Gson GSON = new Gson(); + private static final Type INPUTS_TYPE = new TypeToken>() {}.getType(); + private GearProvenance() {} public static void stamp(ItemStack stack, GearType type, Collection parts) { @@ -38,6 +48,40 @@ public static void stamp(ItemStack stack, GearType type, Collection par stack.setItemMeta(meta); } + /** Records what the craft actually charged. Empty when staff bypassed the cost. */ + public static void stampInputs(ItemStack stack, Map charged) { + if (stack == null || !stack.hasItemMeta()) { + return; + } + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + return; + } + meta.getPersistentDataContainer().set( + GearKeys.craftInputs(), PersistentDataType.STRING, GSON.toJson(charged == null ? Map.of() : charged)); + stack.setItemMeta(meta); + } + + /** Materials charged at craft time, or null for weapons crafted before this was recorded. */ + public static Map readInputs(ItemStack stack) { + if (stack == null || !stack.hasItemMeta()) { + return null; + } + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + return null; + } + String json = meta.getPersistentDataContainer().get(GearKeys.craftInputs(), PersistentDataType.STRING); + if (json == null) { + return null; + } + try { + return GSON.fromJson(json, INPUTS_TYPE); + } catch (JsonParseException ex) { + return null; + } + } + public static int majorityOf(ItemStack stack) { if (stack == null || !stack.hasItemMeta()) { return 0; diff --git a/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java b/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java index 333634c..6c235a8 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java @@ -140,6 +140,7 @@ private static void copyGearPdc(ItemStack from, ItemStack to) { ItemMeta fromMeta = from.getItemMeta(); ItemMeta toMeta = to.getItemMeta(); copyString(fromMeta, toMeta, GearKeys.parts()); + copyString(fromMeta, toMeta, GearKeys.craftInputs()); copyString(fromMeta, toMeta, GearKeys.archetype()); copyString(fromMeta, toMeta, GearKeys.orphans()); Integer archetypeRevision = fromMeta.getPersistentDataContainer().get( diff --git a/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java b/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java index 7d73ab6..11a184e 100644 --- a/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java +++ b/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java @@ -24,6 +24,7 @@ import net.tfminecraft.magic.gear.GearCosts; import net.tfminecraft.magic.gear.GearItemBuilder; import net.tfminecraft.magic.gear.GearKeys; +import net.tfminecraft.magic.gear.GearProvenance; import net.tfminecraft.magic.gear.GearStationStore; import net.tfminecraft.magic.gear.GearType; import net.tfminecraft.magic.gear.PartDef; @@ -290,6 +291,7 @@ private void tryPrepare(Player player) { boolean staffBypass = GearCosts.bypasses(player); Map charged = staffBypass ? Map.of() : GearCosts.total(parts); GearCosts.take(player, parts); + GearProvenance.stampInputs(prepared, charged); GearStationStore.occupy(station, prepared, player.getUniqueId(), charged); OpenStationManager.clear(player); player.closeInventory(); From 9565aaa7d156a1c56c5e9ba2ae7e445204eacd1a Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:28:46 +0200 Subject: [PATCH 2/2] test: cover the recorded craft inputs Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tfminecraft/magic/gear/GearProvenance.java | 9 +-------- .../tfminecraft/magic/GearItemCoverageTest.java | 9 +++++++++ .../magic/GearProvenanceCoverageTest.java | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java b/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java index 0f163fb..c8e80eb 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java @@ -54,9 +54,6 @@ public static void stampInputs(ItemStack stack, Map charged) { return; } ItemMeta meta = stack.getItemMeta(); - if (meta == null) { - return; - } meta.getPersistentDataContainer().set( GearKeys.craftInputs(), PersistentDataType.STRING, GSON.toJson(charged == null ? Map.of() : charged)); stack.setItemMeta(meta); @@ -67,11 +64,7 @@ public static Map readInputs(ItemStack stack) { if (stack == null || !stack.hasItemMeta()) { return null; } - ItemMeta meta = stack.getItemMeta(); - if (meta == null) { - return null; - } - String json = meta.getPersistentDataContainer().get(GearKeys.craftInputs(), PersistentDataType.STRING); + String json = stack.getItemMeta().getPersistentDataContainer().get(GearKeys.craftInputs(), PersistentDataType.STRING); if (json == null) { return null; } diff --git a/src/test/java/net/tfminecraft/magic/GearItemCoverageTest.java b/src/test/java/net/tfminecraft/magic/GearItemCoverageTest.java index 194cf17..278ca02 100644 --- a/src/test/java/net/tfminecraft/magic/GearItemCoverageTest.java +++ b/src/test/java/net/tfminecraft/magic/GearItemCoverageTest.java @@ -199,6 +199,15 @@ void socketRewriteKeepsIdentityAndLocksFirstAttunement() throws Exception { minimal, dest); assertEquals(GearType.STAFF, GearProvenance.archetypeOf(dest)); + assertNull(GearProvenance.readInputs(dest)); + GearProvenance.stampInputs(minimal, Map.of("v.gold", 3)); + invoke( + GearItemBuilder.class, + "copyGearPdc", + new Class[] {ItemStack.class, ItemStack.class}, + minimal, + dest); + assertEquals(Map.of("v.gold", 3), GearProvenance.readInputs(dest)); for (ItemStack[] pair : new ItemStack[][] { {null, dest}, diff --git a/src/test/java/net/tfminecraft/magic/GearProvenanceCoverageTest.java b/src/test/java/net/tfminecraft/magic/GearProvenanceCoverageTest.java index 510e0a7..5a51fd2 100644 --- a/src/test/java/net/tfminecraft/magic/GearProvenanceCoverageTest.java +++ b/src/test/java/net/tfminecraft/magic/GearProvenanceCoverageTest.java @@ -37,6 +37,22 @@ void unstampedItemsAndMissingMetadataAreSafe() { assertTrue(GearProvenance.socketsLocked(item)); } + @Test + void craftInputsRecordWhatWasChargedAndReadBack() { + for (ItemStack item : Arrays.asList(null, new ItemStack(Material.AIR))) { + GearProvenance.stampInputs(item, Map.of("v.gold", 1)); + assertNull(GearProvenance.readInputs(item)); + } + var item = item(); + assertNull(GearProvenance.readInputs(item), "Weapons crafted before the record have none"); + GearProvenance.stampInputs(item, null); + assertEquals(Map.of(), GearProvenance.readInputs(item)); + GearProvenance.stampInputs(item, Map.of("m.materials.steel_ingot", 4, "v.gold", 2)); + assertEquals(Map.of("m.materials.steel_ingot", 4, "v.gold", 2), GearProvenance.readInputs(item)); + tag(item, GearKeys.craftInputs(), PersistentDataType.STRING, "{not json"); + assertNull(GearProvenance.readInputs(item)); + } + @Test void stampingTracksPartAndArchetypeRevisions() { var item = item();