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"); + } +}