0.3.0: villager trade rebalance via VillagerTradesEvent
Build / build (push) Has been cancelled

Hooks NeoForge's VillagerTradesEvent + WandererTradesEvent at data-pack
reload time and wraps every existing ItemListing so the resulting
MerchantOffer has its emerald-cost-or-result items swapped to
Numismatics coin equivalents:

  1..7  emeralds  -> spurs (1:1)
  8..63 emeralds  -> bevels (1 bevel = 8 spurs)
  64+   emeralds  -> cogs (1 cog = 64 spurs)

Lives in bnstoolkit instead of a third-party mod because the obvious
choice — Modrinth's 'numismatics-villager-currency' — requires NeoForge
21.1.230+ and we're on 21.1.228. The two-page Java wrapper here is
simpler than the alternatives (writing every trade override JSON via
'data-trades') AND doesn't push us into a runtime upgrade.

Includes wanderer trades. Modded villager professions are picked up
automatically since we just wrap the event's trade list as-is.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-07 02:38:49 +00:00
parent afe7d38827
commit 10e9980bfd
2 changed files with 130 additions and 1 deletions
+1 -1
View File
@@ -19,5 +19,5 @@ loader_version_range=[1,)
mod_id=bnstoolkit
mod_name=Brass and Sigil Toolkit
mod_license=All Rights Reserved
mod_version=0.2.0
mod_version=0.3.0
mod_group_id=uk.sijbers.bnstoolkit
@@ -0,0 +1,129 @@
package uk.sijbers.bnstoolkit.economy;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.npc.VillagerTrades;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.trading.ItemCost;
import net.minecraft.world.item.trading.MerchantOffer;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.village.VillagerTradesEvent;
import net.neoforged.neoforge.event.village.WandererTradesEvent;
import uk.sijbers.bnstoolkit.BnsToolkit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Server-side villager-trade rebalance.
*
* Hooks {@link VillagerTradesEvent} and {@link WandererTradesEvent} at
* data-pack-reload time. For every existing trade listing, swaps any
* emerald cost/result for the Numismatics equivalent (1 spur = 1
* emerald), so the entire vanilla and modded villager trade table
* routes through the BNS economy without us having to enumerate every
* profession + level by hand.
*
* Counts >= 8 are upgraded to higher denominations so inventory doesn't
* fill up with raw spurs:
* 1..7 spurs (1 each)
* 8..63 -> bevels (1 bevel = 8 spurs)
* 64.. -> cogs (1 cog = 64 spurs)
*
* Why this lives in bnstoolkit rather than a third-party mod:
* - The two existing options on Modrinth (numismatics-villager-currency
* and data-trades) either require a NeoForge bump (1.1.0 wants
* 21.1.230+, we're on 228) or need hand-written JSON for every
* trade. Reusing the event we already need for HUD pushes is
* simpler and avoids touching the live NeoForge install.
*/
@EventBusSubscriber(modid = BnsToolkit.MOD_ID)
public final class VillagerTradeRebalance {
private VillagerTradeRebalance() {}
private static final ResourceLocation SPUR_ID = ResourceLocation.parse("numismatics:spur");
private static final ResourceLocation BEVEL_ID = ResourceLocation.parse("numismatics:bevel");
private static final ResourceLocation COG_ID = ResourceLocation.parse("numismatics:cog");
private static final ResourceLocation EMERALD_ID = ResourceLocation.parse("minecraft:emerald");
@SubscribeEvent
public static void onVillagerTrades(VillagerTradesEvent event) {
Int2ObjectMap<List<VillagerTrades.ItemListing>> trades = event.getTrades();
for (Int2ObjectMap.Entry<List<VillagerTrades.ItemListing>> e : trades.int2ObjectEntrySet()) {
List<VillagerTrades.ItemListing> orig = e.getValue();
List<VillagerTrades.ItemListing> wrapped = new ArrayList<>(orig.size());
for (VillagerTrades.ItemListing inner : orig) wrapped.add(new SpurTradeWrapper(inner));
e.setValue(wrapped);
}
BnsToolkit.LOG.info("[bns/trades] rebalanced trades for profession {}", event.getType().name());
}
@SubscribeEvent
public static void onWandererTrades(WandererTradesEvent event) {
rewrapList(event.getGenericTrades());
rewrapList(event.getRareTrades());
BnsToolkit.LOG.info("[bns/trades] rebalanced wanderer trades");
}
private static void rewrapList(List<VillagerTrades.ItemListing> list) {
List<VillagerTrades.ItemListing> copy = new ArrayList<>(list);
list.clear();
for (VillagerTrades.ItemListing inner : copy) list.add(new SpurTradeWrapper(inner));
}
/** Per-listing wrapper that rewrites the offer's emerald slots on each call. */
private record SpurTradeWrapper(VillagerTrades.ItemListing inner) implements VillagerTrades.ItemListing {
@Override
public MerchantOffer getOffer(net.minecraft.world.entity.Entity entity, net.minecraft.util.RandomSource rand) {
MerchantOffer base = inner.getOffer(entity, rand);
if (base == null) return null;
ItemCost costA = base.getItemCostA();
Optional<ItemCost> costB = base.getItemCostB();
ItemStack result = base.getResult();
ItemCost newCostA = swapIfEmerald(costA);
Optional<ItemCost> newCostB = costB.map(VillagerTradeRebalance::swapIfEmerald);
ItemStack newResult = swapIfEmerald(result);
return new MerchantOffer(newCostA, newCostB, newResult,
base.getUses(), base.getMaxUses(), base.getXp(),
base.getPriceMultiplier(), base.getDemand());
}
}
private static ItemCost swapIfEmerald(ItemCost cost) {
if (cost == null) return null;
ItemStack peek = cost.itemStack();
ResourceLocation id = BuiltInRegistries.ITEM.getKey(peek.getItem());
if (!EMERALD_ID.equals(id)) return cost;
int count = peek.getCount();
CoinSwap swap = pickDenom(count);
Item coinItem = BuiltInRegistries.ITEM.get(swap.itemId);
if (coinItem == Items.AIR) return cost; // mod missing — leave vanilla cost
return new ItemCost(coinItem, swap.count);
}
private static ItemStack swapIfEmerald(ItemStack stack) {
if (stack == null || stack.isEmpty()) return stack;
ResourceLocation id = BuiltInRegistries.ITEM.getKey(stack.getItem());
if (!EMERALD_ID.equals(id)) return stack;
int count = stack.getCount();
CoinSwap swap = pickDenom(count);
Item coinItem = BuiltInRegistries.ITEM.get(swap.itemId);
if (coinItem == Items.AIR) return stack;
return new ItemStack(coinItem, swap.count);
}
private record CoinSwap(ResourceLocation itemId, int count) {}
private static CoinSwap pickDenom(int emeralds) {
if (emeralds >= 64) return new CoinSwap(COG_ID, Math.max(1, emeralds / 64));
if (emeralds >= 8) return new CoinSwap(BEVEL_ID, Math.max(1, emeralds / 8));
return new CoinSwap(SPUR_ID, Math.max(1, emeralds));
}
}