From f0719a3a2acd9a3351aad1312e19e11ddce3f4ac Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:15:06 +0200 Subject: [PATCH] fix: keep alloys working after `ac reload` `ac reload` rebuilt ingredient types, hits and schemes as new objects, but loaded alloys and open stations kept the old ones, and stations looked them up by object. Every alloy then stopped counting as its type until a restart: `ac give equipment ... alloy.` said "0 out of 4 Metal items" and adding an alloy to a station threw. Ingredient types, hit types and hits now compare by id, and a reload points the loaded alloys at the reloaded objects. Co-Authored-By: Claude Opus 5.5 --- .../advancedcrafting/AdvancedCrafting.java | 1 + .../managers/AlloyManager.java | 6 + .../objects/crafting/hits/CraftingHit.java | 13 +- .../objects/crafting/hits/HitType.java | 11 ++ .../objects/data/AlloyData.java | 24 +++ .../objects/ingredients/IngredientType.java | 11 ++ .../advancedcrafting/AlloyReloadTest.java | 140 ++++++++++++++++++ 7 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/tfminecraft/advancedcrafting/AlloyReloadTest.java diff --git a/src/main/java/net/tfminecraft/advancedcrafting/AdvancedCrafting.java b/src/main/java/net/tfminecraft/advancedcrafting/AdvancedCrafting.java index ecd964d..cc0de8b 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/AdvancedCrafting.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/AdvancedCrafting.java @@ -186,6 +186,7 @@ public void createConfigs() { public void reload() { loadConfigs(); + AlloyManager.relinkAlloys(); revisionTracker.flush(); } // Keep the existing legacy text representation, formatting, and exact-string comparisons. diff --git a/src/main/java/net/tfminecraft/advancedcrafting/managers/AlloyManager.java b/src/main/java/net/tfminecraft/advancedcrafting/managers/AlloyManager.java index d83227b..52eba4d 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/managers/AlloyManager.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/managers/AlloyManager.java @@ -58,6 +58,12 @@ public static void removeAlloy(String id) { public static void addAlloy(Alloy a) { alloys.put(a.getId(), a); } + /** After a config reload, so alloys use the reloaded types, hits and schemes. */ + public static void relinkAlloys() { + for (Alloy a : alloys.values()) { + a.getData().relink(); + } + } public boolean hasStation(Location loc) { return stations.containsKey(loc); } diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/CraftingHit.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/CraftingHit.java index 544230e..163360d 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/CraftingHit.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/CraftingHit.java @@ -32,6 +32,15 @@ public String getName() { public HitType getType() { return type; } - - + + // By id: a reload builds new objects, while alloys and open stations still hold the old ones. + @Override + public boolean equals(Object obj) { + return obj instanceof CraftingHit other && id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } } diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/HitType.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/HitType.java index 5f6ee6e..d809a5e 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/HitType.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/hits/HitType.java @@ -18,4 +18,15 @@ public String getId() { public String getName() { return name; } + + // By id: a reload builds new objects, while open stations still hold the old ones. + @Override + public boolean equals(Object obj) { + return obj instanceof HitType other && id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } } diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/data/AlloyData.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/data/AlloyData.java index d5a95bf..d47ffb3 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/data/AlloyData.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/data/AlloyData.java @@ -4,8 +4,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; +import net.tfminecraft.advancedcrafting.loaders.HitLoader; +import net.tfminecraft.advancedcrafting.loaders.SchemeLoader; +import net.tfminecraft.advancedcrafting.loaders.TypeLoader; import net.tfminecraft.advancedcrafting.objects.crafting.hits.CraftingHit; import net.tfminecraft.advancedcrafting.objects.ingredients.Ingredient; import net.tfminecraft.advancedcrafting.objects.ingredients.IngredientType; @@ -121,6 +125,26 @@ public int getTier() { return tier; } + /** After a config reload: swaps in the reloaded type, hits and schemes, keeping any that are gone. */ + public void relink() { + type = relinked(type, IngredientType::getId, TypeLoader::getIngredientTypeByString); + colourScheme = relinked(colourScheme, ColourScheme::getId, SchemeLoader::getColourSchemeByString); + modelScheme = relinked(modelScheme, ModelScheme::getId, SchemeLoader::getModelSchemeByString); + HashMap current = new HashMap<>(); + hits.forEach((hit, amount) -> current.put(relinked(hit, CraftingHit::getId, HitLoader::getByString), amount)); + // Cleared first: put() would keep the old key, which equals the new one. + hits.clear(); + hits.putAll(current); + } + + private static T relinked(T old, Function id, Function lookup) { + if (old == null) { + return null; + } + T fresh = lookup.apply(id.apply(old)); + return fresh != null ? fresh : old; + } + public String buildRevisionContent() { StringBuilder sb = new StringBuilder(); sb.append("xp=").append(xp != null ? xp : "").append(';'); diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/ingredients/IngredientType.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/ingredients/IngredientType.java index 49fcc9e..bc7a150 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/ingredients/IngredientType.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/ingredients/IngredientType.java @@ -20,4 +20,15 @@ public String getId() { public String getName() { return name; } + + // By id: a reload builds new objects, while alloys and open stations still hold the old ones. + @Override + public boolean equals(Object obj) { + return obj instanceof IngredientType other && id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } } diff --git a/src/test/java/net/tfminecraft/advancedcrafting/AlloyReloadTest.java b/src/test/java/net/tfminecraft/advancedcrafting/AlloyReloadTest.java new file mode 100644 index 0000000..78e23ea --- /dev/null +++ b/src/test/java/net/tfminecraft/advancedcrafting/AlloyReloadTest.java @@ -0,0 +1,140 @@ +package net.tfminecraft.advancedcrafting; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.*; +import net.tfminecraft.advancedcrafting.enums.StationFeedback; +import net.tfminecraft.advancedcrafting.loaders.*; +import net.tfminecraft.advancedcrafting.managers.AlloyManager; +import net.tfminecraft.advancedcrafting.objects.alloys.Alloy; +import net.tfminecraft.advancedcrafting.objects.crafting.CraftingStation; +import net.tfminecraft.advancedcrafting.objects.crafting.hits.*; +import net.tfminecraft.advancedcrafting.objects.data.*; +import net.tfminecraft.advancedcrafting.objects.ingredients.IngredientType; +import net.tfminecraft.advancedcrafting.objects.schemes.*; +import net.tfminecraft.advancedcrafting.utils.PDCKeys; +import org.bukkit.*; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.junit.jupiter.api.Test; + +/** {@code ac reload} builds new types, hits and schemes; alloys loaded before it must keep working. */ +class AlloyReloadTest extends CoverageSupport { + ItemStack alloyItem(String id) { + var i = new ItemStack(Material.IRON_INGOT); + var m = i.getItemMeta(); + m.getPersistentDataContainer().set(PDCKeys.alloyId(), PersistentDataType.STRING, id); + i.setItemMeta(m); + return i; + } + + Alloy alloy(String id, IngredientType type, HashMap hits) { + var data = + new AlloyData( + SchemeLoader.colours.get("default"), + 2, + type, + SchemeLoader.models.get("default"), + new StatData(), + hits, + null, + null, + 1, + null); + var alloy = new Alloy(id, id, data); + AlloyManager.addAlloy(alloy); + return alloy; + } + + void reloadTypesAndHits() throws Exception { + TypeLoader.map.put("metal", new IngredientType("metal", yaml("name: Metal"))); + TypeLoader.hMap.put("hammer", new HitType("hammer", yaml("name: Hammer"))); + HitLoader.map.put( + "strike", new CraftingHit("strike", yaml("name: Strike\ntype: hammer\ntool: v.iron_axe"))); + } + + @Test + void alloyLoadedBeforeReloadStillFillsAStation() throws Exception { + alloy( + "darksteel", + TypeLoader.map.get("metal"), + new HashMap<>(Map.of(HitLoader.map.get("strike"), 2))); + reloadTypesAndHits(); + var p = server.addPlayer(); + p.setOp(true); + var station = new CraftingStation(new Location(server.addSimpleWorld("world"), 1, 2, 3)); + station.setRecipe(recipe("recipe: ['metal.1']")); + var item = alloyItem("darksteel"); + p.getInventory().setItemInMainHand(item); + assertEquals(StationFeedback.SUCCESS, station.addMaterial(p, item)); + assertTrue(station.hasAllMaterials(p)); + assertEquals(2, station.getHits().get(HitLoader.map.get("strike")).getNeeded()); + // `ac give equipment ... alloy.` fills a station this way. + var given = + new CraftingStation( + station.getLoc(), + recipe("recipe: ['metal.4']"), + new HashMap<>(Map.of("alloy.darksteel", 4)), + new HashMap<>()); + assertEquals(4, given.getTypes().get(TypeLoader.map.get("metal")).getCurrent()); + } + + @Test + void relinkSwapsInReloadedObjectsAndKeepsMissingOnes() throws Exception { + var oldStrike = HitLoader.map.get("strike"); + var gone = new CraftingHit("gone", yaml("name: Gone\ntype: hammer\ntool: v.stick")); + var hits = new HashMap(); + hits.put(oldStrike, 2); + hits.put(gone, 1); + hits.put(null, 4); + var darksteel = alloy("darksteel", TypeLoader.map.get("metal"), hits).getData(); + var typeless = alloy("typeless", null, new HashMap<>()).getData(); + TypeLoader.map.put("wood", new IngredientType("wood", yaml("name: Wood"))); + var wooden = alloy("wooden", TypeLoader.map.get("wood"), new HashMap<>()).getData(); + var oldWood = TypeLoader.map.remove("wood"); + reloadTypesAndHits(); + SchemeLoader.colours.put( + "default", new ColourScheme("default", yaml("models: [3]\ncolours: ['#000000']"))); + SchemeLoader.models.put("default", new ModelScheme("default", yaml("models: ['sword(8)']"))); + + AlloyManager.relinkAlloys(); + + assertSame(TypeLoader.map.get("metal"), darksteel.getType()); + assertSame(SchemeLoader.colours.get("default"), darksteel.getColourScheme()); + assertSame(SchemeLoader.models.get("default"), darksteel.getModelScheme()); + assertSame(hits, darksteel.getHits()); + assertEquals(3, hits.size()); + for (var hit : hits.keySet()) { + if (hit != null && hit.getId().equals("strike")) assertSame(HitLoader.map.get("strike"), hit); + } + assertNotSame(oldStrike, HitLoader.map.get("strike")); + assertEquals(2, hits.get(HitLoader.map.get("strike"))); + assertEquals(1, hits.get(gone)); + assertEquals(4, hits.get(null)); + assertNull(typeless.getType()); + assertSame(oldWood, wooden.getType()); + } + + @Test + void typesAndHitsCompareById() throws Exception { + var metal = new IngredientType("metal", yaml("name: Metal")); + var renamed = new IngredientType("metal", yaml("name: Iron")); + assertEquals(metal, renamed); + assertEquals(metal.hashCode(), renamed.hashCode()); + assertNotEquals(metal, new IngredientType("wood", yaml("name: Metal"))); + assertNotEquals(metal, "metal"); + + var hammer = new HitType("hammer", yaml("name: Hammer")); + assertEquals(hammer, new HitType("hammer", yaml("name: Mallet"))); + assertEquals(hammer.hashCode(), new HitType("hammer", yaml("name: Mallet")).hashCode()); + assertNotEquals(hammer, new HitType("chisel", yaml("name: Hammer"))); + assertNotEquals(hammer, "hammer"); + + var strike = new CraftingHit("strike", yaml("name: Strike\ntype: hammer\ntool: v.iron_axe")); + var moved = new CraftingHit("strike", yaml("name: Blow\ntype: hammer\ntool: v.stick")); + assertEquals(strike, moved); + assertEquals(strike.hashCode(), moved.hashCode()); + assertNotEquals(strike, new CraftingHit("tap", yaml("name: Strike\ntype: hammer"))); + assertNotEquals(strike, "strike"); + } +}