From b402088e832c939cf9121f1184988e7bc8081d8d Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:37:42 +0200 Subject: [PATCH 1/2] Recycle spent artifact husks at half their rarity's dust Magic 0.9.0 crumbles worn-out artifacts into aura-less husks. They skip min_muffle and return artifact_returns.husk_fraction (default 0.5) of the rarity amount, rounded down. Needs Magic 0.9.0. Co-Authored-By: Claude Opus 5.5 --- pom.xml | 3 +- .../java/net/tfminecraft/recycler/Cache.java | 2 ++ .../recycler/loader/ConfigLoader.java | 4 +++ .../recycler/provider/ArtifactProvider.java | 16 ++++++++- src/main/resources/config.yml | 4 +++ .../recycler/ArtifactProviderTest.java | 33 +++++++++++++++++++ .../net/tfminecraft/recycler/LoadersTest.java | 9 +++-- 7 files changed, 67 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index e528fb6..1d53045 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,8 @@ 2.2.5 2.2.5 2.0.6 - 0.4.7 + + 0.9.0 2.6.0 diff --git a/src/main/java/net/tfminecraft/recycler/Cache.java b/src/main/java/net/tfminecraft/recycler/Cache.java index d250790..46ad1ec 100644 --- a/src/main/java/net/tfminecraft/recycler/Cache.java +++ b/src/main/java/net/tfminecraft/recycler/Cache.java @@ -29,6 +29,8 @@ private Cache() {} "common", 4, "uncommon", 8, "rare", 12, "epic", 16, "legendary", 28); public static int artifactDefaultReturn = 4; public static double artifactMinMuffle = 0.0; + /** Share of the rarity amount a spent artifact husk returns, rounded down. */ + public static double artifactHuskFraction = 0.5; public static boolean blockConfirmWhenZeroYield = true; public static boolean depositWhitelistMode = false; diff --git a/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java b/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java index cdb7ec5..8460eaf 100644 --- a/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java @@ -19,6 +19,7 @@ public final class ConfigLoader implements LoaderInterface { private static final double DEFAULT_SCRAP_RATE = 0.5; private static final double DEFAULT_RECIPE_RATE = 1.0; private static final String DEFAULT_ARTIFACT_ITEM = "m.currency.enchanted_dust"; + private static final double DEFAULT_HUSK_FRACTION = 0.5; private static final java.util.Map DEFAULT_ARTIFACT_RETURNS = java.util.Map.of( "common", 4, "uncommon", 8, "rare", 12, "epic", 16, "legendary", 28); @@ -151,8 +152,11 @@ private static void applyArtifactReturns(ConfigurationSection section) { Cache.artifactReturnItem = DEFAULT_ARTIFACT_ITEM; Cache.artifactDefaultReturn = 4; Cache.artifactMinMuffle = 0.0; + Cache.artifactHuskFraction = DEFAULT_HUSK_FRACTION; if (section != null) { Cache.artifactMinMuffle = readRate(section, "min_muffle", 0.0, 0.0); + Cache.artifactHuskFraction = readRate( + section, "husk_fraction", DEFAULT_HUSK_FRACTION, DEFAULT_HUSK_FRACTION); Cache.artifactReturnItem = section.getString("item", DEFAULT_ARTIFACT_ITEM).trim(); Cache.artifactDefaultReturn = readAmount(section, "default", 4); ConfigurationSection rarities = section.getConfigurationSection("rarities"); diff --git a/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java b/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java index c8fd852..fe40a49 100644 --- a/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java +++ b/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java @@ -10,6 +10,7 @@ import net.tfminecraft.magic.artifact.Artifact; import net.tfminecraft.magic.artifact.ArtifactCareStore; import net.tfminecraft.magic.artifact.ArtifactKeys; +import net.tfminecraft.magic.artifact.ArtifactWear; import net.tfminecraft.recycler.Cache; import net.tfminecraft.recycler.model.RecycleOutput; @@ -17,6 +18,10 @@ * Magic artifacts muffled at least {@code artifact_returns.min_muffle} (0 = any artifact). * They are found rather than crafted, so there is no record of what went into them: the * return is a fixed amount by rarity from {@code artifact_returns}. Stored aura is not returned. + * + *

Spent husks (artifacts worn out in meditation) hold no aura, so min_muffle does not + * apply to them. They return {@code artifact_returns.husk_fraction} of their rarity's amount, + * rounded down. */ public final class ArtifactProvider implements RecycleProvider { @@ -32,6 +37,9 @@ public double returnRate() { @Override public boolean canHandle(ItemStack item) { + if (ArtifactWear.isHusk(item)) { + return true; + } return Artifact.fromItem(item) != null && !belowMinMuffle(item); } @@ -41,6 +49,9 @@ public boolean canHandle(ItemStack item) { * artifact cannot get around either setting. */ public boolean refuses(ItemStack item) { + if (ArtifactWear.isHusk(item)) { + return outputsFor(item).isEmpty(); + } return Artifact.fromItem(item) != null && (belowMinMuffle(item) || outputsFor(item).isEmpty()); } @@ -50,7 +61,7 @@ private static boolean belowMinMuffle(ItemStack item) { @Override public List resolveBaseOutputs(ItemStack item) { - if (Artifact.fromItem(item) == null) { + if (!ArtifactWear.isHusk(item) && Artifact.fromItem(item) == null) { return List.of(); } return outputsFor(item); @@ -58,6 +69,9 @@ public List resolveBaseOutputs(ItemStack item) { private static List outputsFor(ItemStack item) { int amount = Cache.artifactRarityReturns.getOrDefault(rarity(item), Cache.artifactDefaultReturn); + if (ArtifactWear.isHusk(item)) { + amount = (int) Math.floor(amount * Cache.artifactHuskFraction); + } if (amount <= 0 || Cache.artifactReturnItem.isBlank()) { return List.of(); } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5541b13..90100a8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -60,11 +60,15 @@ artifact_returns: # Lowest muffle (0.0-1.0) an artifact needs before it can be recycled. 0.0 accepts any # artifact; 1.0 accepts only fully muffled ones (about a day in a chest). min_muffle: 0.0 + # Spent husks (artifacts worn out in meditation) return this share of their rarity's + # amount, rounded down. min_muffle does not apply to them. 0.0-1.0. + husk_fraction: 0.5 item: m.currency.enchanted_dust rarities: common: 4 uncommon: 8 rare: 12 + # Magic no longer rolls Epic; this keeps older Epic artifacts recycling at their value. epic: 16 legendary: 28 default: 4 diff --git a/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java b/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java index 752628c..f29821d 100644 --- a/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java +++ b/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java @@ -36,6 +36,7 @@ void artifactsReturnDustByRarityAndMinMuffleGatesThem() { var unrecorded = artifact(null); try (var art = mockStatic(Artifact.class); var keys = mockStatic(ArtifactKeys.class); + var wear = mockStatic(ArtifactWear.class); var care = mockStatic(ArtifactCareStore.class)) { keys.when(ArtifactKeys::artifactRarity).thenReturn(RARITY); var found = mock(Artifact.class); @@ -69,6 +70,37 @@ void artifactsReturnDustByRarityAndMinMuffleGatesThem() { } } + @Test + void spentHusksReturnTheirShareOfTheRarityAmountWhateverTheirMuffle() { + var p = new ArtifactProvider(); + var legendary = artifact("legendary"); + var common = artifact("common"); + try (var art = mockStatic(Artifact.class); + var keys = mockStatic(ArtifactKeys.class); + var wear = mockStatic(ArtifactWear.class); + var care = mockStatic(ArtifactCareStore.class)) { + keys.when(ArtifactKeys::artifactRarity).thenReturn(RARITY); + wear.when(() -> ArtifactWear.isHusk(legendary)).thenReturn(true); + wear.when(() -> ArtifactWear.isHusk(common)).thenReturn(true); + Cache.artifactMinMuffle = 1.0; + assertTrue(p.canHandle(legendary), "A husk has no aura, so min_muffle does not apply"); + assertFalse(p.refuses(legendary)); + assertEquals( + List.of(new RecycleOutput("m.currency.enchanted_dust", 14)), + p.resolveBaseOutputs(legendary)); + Cache.artifactHuskFraction = .3; + assertEquals( + List.of(new RecycleOutput("m.currency.enchanted_dust", 1)), + p.resolveBaseOutputs(common), + "Rounded down"); + Cache.artifactHuskFraction = 0; + assertTrue(p.refuses(common), "A husk worth nothing is refused, not sent to recipes"); + assertTrue(p.resolveBaseOutputs(common).isEmpty()); + art.verifyNoInteractions(); + care.verifyNoInteractions(); + } + } + @Test void chainRefusesArtifactsBelowMinMuffleEvenWhenARecipeMatches() throws Exception { yaml("recipes.yml", "recipes:\n rod:\n input: v.blaze_rod\n outputs: ['v.stick 1']"); @@ -81,6 +113,7 @@ void chainRefusesArtifactsBelowMinMuffleEvenWhenARecipeMatches() throws Exceptio try (var art = mockStatic(Artifact.class); var keys = mockStatic(ArtifactKeys.class); var care = mockStatic(ArtifactCareStore.class); + var wear = mockStatic(ArtifactWear.class); var gear = mockStatic(net.tfminecraft.magic.gear.GearProvenance.class)) { keys.when(ArtifactKeys::artifactRarity).thenReturn(RARITY); art.when(() -> Artifact.fromItem(stack)).thenReturn(mock(Artifact.class)); diff --git a/src/test/java/net/tfminecraft/recycler/LoadersTest.java b/src/test/java/net/tfminecraft/recycler/LoadersTest.java index fd6d351..4ad39d4 100644 --- a/src/test/java/net/tfminecraft/recycler/LoadersTest.java +++ b/src/test/java/net/tfminecraft/recycler/LoadersTest.java @@ -89,17 +89,19 @@ void artifactReturnsUseDefaultsOverridesAndClampNegatives() throws Exception { Cache.artifactRarityReturns); assertEquals(4, Cache.artifactDefaultReturn); assertEquals(0, Cache.artifactMinMuffle, "Any artifact is accepted by default"); + assertEquals(.5, Cache.artifactHuskFraction, "Husks return half by default"); loader.loadSafe( yaml( "artifacts.yml", "return_rates:\n artifacts: 0.5\nartifact_returns:\n item: ' m.currency.other '\n" - + " min_muffle: 2\n default: -2\n rarities:\n Common: 0\n mythic: 9\n rare: -1\n" + + " min_muffle: 2\n husk_fraction: 3\n default: -2\n rarities:\n Common: 0\n mythic: 9\n rare: -1\n" + " legendary: seven\n odd: x\n") .toFile()); assertEquals(.5, Cache.artifactReturnRate); assertEquals("m.currency.other", Cache.artifactReturnItem); assertEquals(0, Cache.artifactDefaultReturn, "Negative amounts become 0"); assertEquals(1, Cache.artifactMinMuffle, "min_muffle is clamped to 0.0-1.0"); + assertEquals(1, Cache.artifactHuskFraction, "husk_fraction is clamped to 0.0-1.0"); assertEquals( Map.of( "common", 0, "uncommon", 8, "rare", 0, "epic", 16, "legendary", 28, "mythic", 9, "odd", @@ -107,15 +109,18 @@ void artifactReturnsUseDefaultsOverridesAndClampNegatives() throws Exception { Cache.artifactRarityReturns, "A non-number keeps the built-in amount, or default for a new rarity"); loader.loadSafe( - yaml("no-rarities.yml", "artifact_returns:\n default: 3\n min_muffle: 0.5\n").toFile()); + yaml("no-rarities.yml", "artifact_returns:\n default: 3\n min_muffle: 0.5\n husk_fraction: 0.25\n") + .toFile()); assertEquals(3, Cache.artifactDefaultReturn); assertEquals(.5, Cache.artifactMinMuffle); + assertEquals(.25, Cache.artifactHuskFraction); assertEquals(28, Cache.artifactRarityReturns.get("legendary")); loader.loadSafe(yaml("none.yml", "{}").toFile()); assertEquals("m.currency.enchanted_dust", Cache.artifactReturnItem); assertEquals(4, Cache.artifactDefaultReturn); assertEquals(4, Cache.artifactRarityReturns.get("common")); assertEquals(0, Cache.artifactMinMuffle); + assertEquals(.5, Cache.artifactHuskFraction); } @Test From 9909b5c747a4847ba7880984a47a28d7c003d151 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:50:35 +0200 Subject: [PATCH 2/2] Keep recycling working against a Magic without husk support The chain asks ArtifactProvider about every item, so touching ArtifactWear on Magic 0.8.x or older would break all recycling. Husk checks now run only when the class is present. Co-Authored-By: Claude Opus 5.5 --- .../recycler/provider/ArtifactProvider.java | 27 ++++++++++++++++--- .../recycler/ArtifactProviderTest.java | 10 +++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java b/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java index fe40a49..ca2b780 100644 --- a/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java +++ b/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java @@ -25,6 +25,25 @@ */ public final class ArtifactProvider implements RecycleProvider { + /** + * Husks exist from Magic 0.9.0. Against an older Magic the class is missing, and + * touching it would break every recycle, since the chain asks this provider first. + */ + public static boolean huskSupport = classPresent("net.tfminecraft.magic.artifact.ArtifactWear"); + + public static boolean classPresent(String name) { + try { + Class.forName(name, false, ArtifactProvider.class.getClassLoader()); + return true; + } catch (ClassNotFoundException | LinkageError ex) { + return false; + } + } + + private static boolean isHusk(ItemStack item) { + return huskSupport && ArtifactWear.isHusk(item); + } + @Override public int priority() { return 16; @@ -37,7 +56,7 @@ public double returnRate() { @Override public boolean canHandle(ItemStack item) { - if (ArtifactWear.isHusk(item)) { + if (isHusk(item)) { return true; } return Artifact.fromItem(item) != null && !belowMinMuffle(item); @@ -49,7 +68,7 @@ public boolean canHandle(ItemStack item) { * artifact cannot get around either setting. */ public boolean refuses(ItemStack item) { - if (ArtifactWear.isHusk(item)) { + if (isHusk(item)) { return outputsFor(item).isEmpty(); } return Artifact.fromItem(item) != null && (belowMinMuffle(item) || outputsFor(item).isEmpty()); @@ -61,7 +80,7 @@ private static boolean belowMinMuffle(ItemStack item) { @Override public List resolveBaseOutputs(ItemStack item) { - if (!ArtifactWear.isHusk(item) && Artifact.fromItem(item) == null) { + if (!isHusk(item) && Artifact.fromItem(item) == null) { return List.of(); } return outputsFor(item); @@ -69,7 +88,7 @@ public List resolveBaseOutputs(ItemStack item) { private static List outputsFor(ItemStack item) { int amount = Cache.artifactRarityReturns.getOrDefault(rarity(item), Cache.artifactDefaultReturn); - if (ArtifactWear.isHusk(item)) { + if (isHusk(item)) { amount = (int) Math.floor(amount * Cache.artifactHuskFraction); } if (amount <= 0 || Cache.artifactReturnItem.isBlank()) { diff --git a/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java b/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java index f29821d..720b7ba 100644 --- a/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java +++ b/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java @@ -98,7 +98,17 @@ void spentHusksReturnTheirShareOfTheRarityAmountWhateverTheirMuffle() { assertTrue(p.resolveBaseOutputs(common).isEmpty()); art.verifyNoInteractions(); care.verifyNoInteractions(); + ArtifactProvider.huskSupport = false; + try { + assertFalse(p.canHandle(common), "Without husk support it is just a non-artifact"); + assertFalse(p.refuses(common)); + assertTrue(p.resolveBaseOutputs(common).isEmpty()); + } finally { + ArtifactProvider.huskSupport = true; + } } + assertTrue(ArtifactProvider.classPresent("net.tfminecraft.magic.artifact.ArtifactWear")); + assertFalse(ArtifactProvider.classPresent("net.tfminecraft.magic.artifact.Missing")); } @Test