diff --git a/build.gradle b/build.gradle
index f96b6341..522a48c4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
plugins {
id "architectury-plugin" version "3.3-SNAPSHOT"
- id "dev.architectury.loom" version "0.7.2-SNAPSHOT" apply false
+ id "dev.architectury.loom" version "0.7.3-SNAPSHOT" apply false
id "org.cadixdev.licenser" version "0.5.0"
id "com.matthewprenger.cursegradle" version "1.4.0" apply false
id "maven-publish"
diff --git a/common/src/main/java/me/shedaniel/architectury/mixin/AbstractVillagerMixin.java b/common/src/main/java/me/shedaniel/architectury/mixin/AbstractVillagerMixin.java
new file mode 100644
index 00000000..e2fc5de5
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/mixin/AbstractVillagerMixin.java
@@ -0,0 +1,102 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.mixin;
+
+import com.google.common.base.MoreObjects;
+import me.shedaniel.architectury.registry.trade.TradeRegistry;
+import me.shedaniel.architectury.registry.trade.impl.OfferMixingContext;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.npc.AbstractVillager;
+import net.minecraft.world.entity.npc.VillagerTrades;
+import net.minecraft.world.item.trading.MerchantOffer;
+import net.minecraft.world.item.trading.MerchantOffers;
+import net.minecraft.world.level.Level;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * {@link AbstractVillager#addOffersFromItemListings(MerchantOffers, VillagerTrades.ItemListing[], int)} creates
+ * a {@link Set} with x random integer from {@link VillagerTrades.ItemListing} array indexes to iterate through.
+ *
+ * If we use {@link TradeRegistry} to remove one offer from a villager
+ * we will end up with just x-1 offers but we still want to have x offers (as long there are enough) for a villager if
+ * there are still {@link VillagerTrades.ItemListing} left.
+ *
+ * To solve this we override the iterator with our own iterator which iterate through all indexes.
+ * As soon {@link OfferMixingContext#maxOffers} offers are created we skip the remaining elements in the iterator {@link OfferMixingContext#skipIteratorIfMaxOffersReached()}.
+ */
+@Mixin(AbstractVillager.class)
+public abstract class AbstractVillagerMixin extends Entity {
+ public AbstractVillagerMixin(EntityType> entityType, Level level) {
+ super(entityType, level);
+ }
+
+ @Unique
+ private final ThreadLocal offerContext = new ThreadLocal<>();
+
+
+ @Redirect(
+ method = "addOffersFromItemListings(Lnet/minecraft/world/item/trading/MerchantOffers;[Lnet/minecraft/world/entity/npc/VillagerTrades$ItemListing;I)V",
+ at = @At(value = "INVOKE", target = "Ljava/util/Set;iterator()Ljava/util/Iterator;")
+ )
+ public Iterator overrideIterator(Set set, MerchantOffers offers, VillagerTrades.ItemListing[] itemListings, int maxOffers) {
+ OfferMixingContext context = new OfferMixingContext(MoreObjects.firstNonNull(architectury$getMaxOfferOverride(), maxOffers), itemListings, random);
+ offerContext.set(context);
+ return context.getIterator();
+ }
+
+ @ModifyVariable(
+ method = "addOffersFromItemListings(Lnet/minecraft/world/item/trading/MerchantOffers;[Lnet/minecraft/world/entity/npc/VillagerTrades$ItemListing;I)V",
+ at = @At(value = "STORE"),
+ ordinal = 0
+ )
+ public MerchantOffer handleOffer(MerchantOffer offer) {
+ OfferMixingContext context = offerContext.get();
+
+ if (offer == null || context.getMaxOffers() == 0) {
+ context.skipIteratorIfMaxOffersReached();
+ return null;
+ }
+
+ MerchantOffer handledOffer = architectury$handleOffer(offer);
+ if (handledOffer != null) {
+ context.skipIteratorIfMaxOffersReached();
+ }
+
+ return handledOffer;
+ }
+
+ public MerchantOffer architectury$handleOffer(MerchantOffer offer) {
+ return offer;
+ }
+
+ @Nullable
+ public Integer architectury$getMaxOfferOverride() {
+ return null;
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/mixin/VillagerMixin.java b/common/src/main/java/me/shedaniel/architectury/mixin/VillagerMixin.java
new file mode 100644
index 00000000..b6f62a94
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/mixin/VillagerMixin.java
@@ -0,0 +1,63 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.mixin;
+
+import me.shedaniel.architectury.registry.trade.VillagerTradeOfferContext;
+import me.shedaniel.architectury.registry.trade.impl.TradeRegistryData;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.npc.Villager;
+import net.minecraft.world.entity.npc.VillagerData;
+import net.minecraft.world.item.trading.MerchantOffer;
+import net.minecraft.world.level.Level;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin(Villager.class)
+public abstract class VillagerMixin extends AbstractVillagerMixin {
+
+ public VillagerMixin(EntityType> entityType, Level level) {
+ super(entityType, level);
+ }
+
+ @Shadow
+ public abstract VillagerData getVillagerData();
+
+ @Override
+ public MerchantOffer architectury$handleOffer(MerchantOffer offer) {
+ VillagerData vd = getVillagerData();
+
+ VillagerTradeOfferContext context = new VillagerTradeOfferContext(vd, offer, this, random);
+
+ boolean removeResult = TradeRegistryData.invokeVillagerOfferRemoving(context);
+ if (removeResult) {
+ return null;
+ }
+
+ TradeRegistryData.invokeVillagerOfferModify(context);
+ return offer;
+ }
+
+ @Override
+ @Nullable
+ public Integer architectury$getMaxOfferOverride() {
+ return TradeRegistryData.getVillagerMaxOffers(getVillagerData().getProfession(), getVillagerData().getLevel());
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/mixin/WanderingTraderMixin.java b/common/src/main/java/me/shedaniel/architectury/mixin/WanderingTraderMixin.java
new file mode 100644
index 00000000..06c41781
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/mixin/WanderingTraderMixin.java
@@ -0,0 +1,89 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.mixin;
+
+import me.shedaniel.architectury.registry.trade.WanderingTraderOfferContext;
+import me.shedaniel.architectury.registry.trade.impl.TradeRegistryData;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.npc.VillagerTrades;
+import net.minecraft.world.entity.npc.WanderingTrader;
+import net.minecraft.world.item.trading.MerchantOffer;
+import net.minecraft.world.level.Level;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+
+@Mixin(WanderingTrader.class)
+public abstract class WanderingTraderMixin extends AbstractVillagerMixin {
+ public WanderingTraderMixin(EntityType> entityType, Level level) {
+ super(entityType, level);
+ }
+
+ @Unique
+ private final ThreadLocal vanillaSelectedItemListing = new ThreadLocal<>();
+
+ @ModifyVariable(
+ method = "updateTrades()V",
+ at = @At(value = "INVOKE_ASSIGN"),
+ ordinal = 0
+ )
+ public VillagerTrades.ItemListing storeItemListing(VillagerTrades.ItemListing itemListing) {
+ vanillaSelectedItemListing.set(itemListing);
+ return itemListing;
+ }
+
+ @ModifyVariable(
+ method = "updateTrades()V",
+ at = @At(value = "INVOKE_ASSIGN"),
+ ordinal = 0
+ )
+ public MerchantOffer handleSecondListingOffer(MerchantOffer offer) {
+ if (offer == null) {
+ return null;
+ }
+
+ return invokeWanderingTraderEvents(offer, true);
+ }
+
+ @Override
+ public MerchantOffer architectury$handleOffer(MerchantOffer offer) {
+ return invokeWanderingTraderEvents(offer, false);
+ }
+
+ @Nullable
+ private MerchantOffer invokeWanderingTraderEvents(MerchantOffer offer, boolean rare) {
+ WanderingTraderOfferContext context = new WanderingTraderOfferContext(offer, rare, this, random);
+ boolean removeResult = TradeRegistryData.invokeWanderingTraderOfferRemoving(context);
+ if (removeResult) {
+ return null;
+ }
+
+ TradeRegistryData.invokeWanderingTraderOfferModify(context);
+ return offer;
+ }
+
+ @Override
+ @Nullable
+ public Integer architectury$getMaxOfferOverride() {
+ return TradeRegistryData.getWanderingTraderMaxOffers();
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/MerchantOfferAccess.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/MerchantOfferAccess.java
new file mode 100644
index 00000000..f02e094f
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/MerchantOfferAccess.java
@@ -0,0 +1,83 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.registry.trade;
+
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.trading.MerchantOffer;
+
+public class MerchantOfferAccess {
+ private final MerchantOffer offer;
+
+ MerchantOfferAccess(MerchantOffer offer) {
+ this.offer = offer;
+ }
+
+ public ItemStack getCostA() {
+ return offer.getBaseCostA();
+ }
+
+ public void setCostA(ItemStack itemStack) {
+ offer.baseCostA = itemStack.copy();
+ }
+
+ public ItemStack getCostB() {
+ return offer.getCostB();
+ }
+
+ public void setCostB(ItemStack itemStack) {
+ offer.costB = itemStack.copy();
+ }
+
+ public ItemStack getResult() {
+ return offer.getResult();
+ }
+
+ public void setResult(ItemStack itemStack) {
+ offer.result = itemStack.copy();
+ }
+
+ public int getMaxUses() {
+ return offer.getMaxUses();
+ }
+
+ public void setMaxUses(int maxUses) {
+ offer.maxUses = maxUses;
+ }
+
+ public float getPriceMultiplier() {
+ return offer.getPriceMultiplier();
+ }
+
+ public void setPriceMultiplier(float priceMultiplier) {
+ offer.priceMultiplier = priceMultiplier;
+ }
+
+ public int getXp() {
+ return offer.getXp();
+ }
+
+ public void setXp(int xp) {
+ offer.xp = xp;
+ }
+
+ public MerchantOffer getOffer() {
+ return offer;
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/SimpleTrade.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/SimpleTrade.java
index 4a532bf9..87f8b6b0 100644
--- a/common/src/main/java/me/shedaniel/architectury/registry/trade/SimpleTrade.java
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/SimpleTrade.java
@@ -50,7 +50,7 @@ public class SimpleTrade implements VillagerTrades.ItemListing {
* You can take a look at all the values the vanilla game uses right here {@link VillagerTrades#TRADES}.
*
* @param primaryPrice The first price a player has to pay to get the 'sale' stack.
- * @param secondaryPrice A optional, secondary price to pay as well as the primary one. If not needed just use {@link ItemStack#EMPTY}.
+ * @param secondaryPrice An optional, secondary price to pay as well as the primary one. If not needed just use {@link ItemStack#EMPTY}.
* @param sale The ItemStack which a player can purchase in exchange for the two prices.
* @param maxTrades The amount of trades one villager or wanderer can do. When the amount is surpassed, the trade can't be purchased anymore.
* @param experiencePoints How much experience points does the player get, when trading. Vanilla uses between 2 and 30 for this.
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/TradeOfferContext.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/TradeOfferContext.java
new file mode 100644
index 00000000..8804bf63
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/TradeOfferContext.java
@@ -0,0 +1,49 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.registry.trade;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.item.trading.MerchantOffer;
+
+import java.util.Random;
+
+public abstract class TradeOfferContext {
+ private final MerchantOfferAccess offer;
+ private final Entity entity;
+ private final Random random;
+
+ public TradeOfferContext(MerchantOffer offer, Entity entity, Random random) {
+ this.offer = new MerchantOfferAccess(offer);
+ this.entity = entity;
+ this.random = random;
+ }
+
+ public MerchantOfferAccess getOffer() {
+ return offer;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public Random getRandom() {
+ return random;
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/TradeRegistry.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/TradeRegistry.java
index 7a393e0e..633001b5 100644
--- a/common/src/main/java/me/shedaniel/architectury/registry/trade/TradeRegistry.java
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/TradeRegistry.java
@@ -20,9 +20,16 @@
package me.shedaniel.architectury.registry.trade;
import dev.architectury.injectables.annotations.ExpectPlatform;
+import me.shedaniel.architectury.registry.trade.impl.TradeRegistryData;
import net.minecraft.world.entity.npc.VillagerProfession;
import net.minecraft.world.entity.npc.VillagerTrades;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
public class TradeRegistry {
private TradeRegistry() {
}
@@ -47,6 +54,71 @@ public class TradeRegistry {
throw new AssertionError();
}
+ /**
+ * Override the max possible offers a villager can have by its profession and level.
+ *
+ * @param profession The Profession of the villager.
+ * @param level The level of the villager. Vanilla range is 1 to 5, however mods may extend that upper limit further.
+ * @param maxOffers Max possible offers a villager can have.
+ */
+ public static void setVillagerMaxOffers(VillagerProfession profession, int level, int maxOffers) {
+ if (level < 1) {
+ throw new IllegalArgumentException("Villager Trade level has to be at least 1!");
+ }
+
+ if (maxOffers < 0) {
+ throw new IllegalArgumentException("Villager's max offers has to be at least 0!");
+ }
+
+ Map map = TradeRegistryData.VILLAGER_MAX_OFFER_OVERRIDES.computeIfAbsent(profession, k -> new HashMap<>());
+ map.put(level, maxOffers);
+ }
+
+
+ /**
+ * Register a callback which provide {@link VillagerTradeOfferContext} to modify the given offer from a villager.
+ * The callback gets called when {@link net.minecraft.world.entity.npc.Villager} generates their offer list.
+ *
+ * @param callback The callback to handle modification for the given offer context.
+ */
+ public static void modifyVillagerOffers(Consumer callback) {
+ Objects.requireNonNull(callback);
+ TradeRegistryData.VILLAGER_MODIFY_HANDLERS.add(callback);
+ }
+
+ /**
+ * Register a filter which provide {@link VillagerTradeOfferContext} to test the given offer from a villager.
+ * The filter gets called when {@link net.minecraft.world.entity.npc.Villager} generates their offer list.
+ *
+ * @param filter The filter to test if an offer should be removed. Returning true means the offer will be removed.
+ */
+ public static void removeVillagerOffers(Predicate filter) {
+ Objects.requireNonNull(filter);
+ TradeRegistryData.VILLAGER_REMOVE_HANDLERS.add(filter);
+ }
+
+ /**
+ * Register a callback which provide {@link WanderingTraderOfferContext} to modify the given offer from the wandering trader.
+ * The callback gets called when {@link net.minecraft.world.entity.npc.WanderingTrader} generates their offer list.
+ *
+ * @param callback The callback to handle modification for the given offer context.
+ */
+ public static void modifyWanderingTraderOffers(Consumer callback) {
+ Objects.requireNonNull(callback);
+ TradeRegistryData.WANDERING_TRADER_MODIFY_HANDLERS.add(callback);
+ }
+
+ /**
+ * Register a filter which provide {@link WanderingTraderOfferContext} to test the given offer from the wandering trader.
+ * The filter gets called when {@link net.minecraft.world.entity.npc.WanderingTrader} generates their offer list.
+ *
+ * @param filter The filter to test if an offer should be removed. Returning true means the offer will be removed.
+ */
+ public static void removeWanderingTraderOffers(Predicate filter) {
+ Objects.requireNonNull(filter);
+ TradeRegistryData.WANDERING_TRADER_REMOVE_HANDLERS.add(filter);
+ }
+
/**
* Register a trade ({@link VillagerTrades.ItemListing}) to a wandering trader by its rarity.
* When the mod loader is Forge, the {@code WandererTradesEvent} event is used.
@@ -59,4 +131,16 @@ public class TradeRegistry {
throw new AssertionError();
}
+ /**
+ * Override the max possible offers the wandering trader can have. This does not affect the rare trade.
+ *
+ * @param maxOffers Max possible offers a villager can have.
+ */
+ public static void setWanderingTraderMaxOffers(int maxOffers) {
+ if (maxOffers < 0) {
+ throw new IllegalArgumentException("Wandering trader's max offers has to be at least 0!");
+ }
+
+ TradeRegistryData.wanderingTraderMaxOfferOverride = maxOffers;
+ }
}
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/VillagerTradeOfferContext.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/VillagerTradeOfferContext.java
new file mode 100644
index 00000000..0087d995
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/VillagerTradeOfferContext.java
@@ -0,0 +1,56 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.registry.trade;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.npc.VillagerData;
+import net.minecraft.world.entity.npc.VillagerProfession;
+import net.minecraft.world.entity.npc.VillagerType;
+import net.minecraft.world.item.trading.MerchantOffer;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Random;
+
+public class VillagerTradeOfferContext extends TradeOfferContext {
+
+ private final VillagerProfession profession;
+ private final int level;
+ private final VillagerType type;
+
+ @ApiStatus.Internal
+ public VillagerTradeOfferContext(VillagerData vd, MerchantOffer offer, Entity entity, Random random) {
+ super(offer, entity, random);
+ this.profession = vd.getProfession();
+ this.level = vd.getLevel();
+ this.type = vd.getType();
+ }
+
+ public VillagerProfession getProfession() {
+ return profession;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public VillagerType getType() {
+ return type;
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/WanderingTraderOfferContext.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/WanderingTraderOfferContext.java
new file mode 100644
index 00000000..520190c1
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/WanderingTraderOfferContext.java
@@ -0,0 +1,21 @@
+package me.shedaniel.architectury.registry.trade;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.item.trading.MerchantOffer;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Random;
+
+public class WanderingTraderOfferContext extends TradeOfferContext {
+ private final boolean rare;
+
+ @ApiStatus.Internal
+ public WanderingTraderOfferContext(MerchantOffer offer, boolean rare, Entity entity, Random random) {
+ super(offer, entity, random);
+ this.rare = rare;
+ }
+
+ public boolean isRare() {
+ return rare;
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/impl/OfferMixingContext.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/impl/OfferMixingContext.java
new file mode 100644
index 00000000..578e5eda
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/impl/OfferMixingContext.java
@@ -0,0 +1,76 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.registry.trade.impl;
+
+import net.minecraft.world.entity.npc.VillagerTrades;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+@ApiStatus.Internal
+public class OfferMixingContext {
+ private int currentIndex;
+ private final int maxOffers;
+ private final Iterator iterator;
+ private final VillagerTrades.ItemListing[] itemListings;
+ private final Random random;
+
+ public OfferMixingContext(int maxOffers, VillagerTrades.ItemListing[] itemListings, Random random) {
+ this.currentIndex = 0;
+ this.maxOffers = Math.min(maxOffers, itemListings.length);
+ this.itemListings = itemListings;
+ this.random = random;
+
+ List shuffled = createShuffledIndexList();
+ this.iterator = shuffled.iterator();
+ }
+
+ public void skipIteratorIfMaxOffersReached() {
+ currentIndex++;
+ if (currentIndex >= getMaxOffers()) {
+ skip();
+ }
+ }
+
+ @NotNull
+ public Iterator getIterator() {
+ return iterator;
+ }
+
+ private void skip() {
+ iterator.forEachRemaining(($) -> {
+ });
+ }
+
+ @NotNull
+ private List createShuffledIndexList() {
+ List shuffledListings = new ArrayList<>();
+ for (int i = 0; i < itemListings.length; i++) {
+ shuffledListings.add(i);
+ }
+ Collections.shuffle(shuffledListings, random);
+ return shuffledListings;
+ }
+
+ public int getMaxOffers() {
+ return maxOffers;
+ }
+}
diff --git a/common/src/main/java/me/shedaniel/architectury/registry/trade/impl/TradeRegistryData.java b/common/src/main/java/me/shedaniel/architectury/registry/trade/impl/TradeRegistryData.java
new file mode 100644
index 00000000..41c17b41
--- /dev/null
+++ b/common/src/main/java/me/shedaniel/architectury/registry/trade/impl/TradeRegistryData.java
@@ -0,0 +1,63 @@
+package me.shedaniel.architectury.registry.trade.impl;
+
+import me.shedaniel.architectury.registry.trade.VillagerTradeOfferContext;
+import me.shedaniel.architectury.registry.trade.WanderingTraderOfferContext;
+import net.minecraft.world.entity.npc.VillagerProfession;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+@ApiStatus.Internal
+public class TradeRegistryData {
+ public static final List> VILLAGER_MODIFY_HANDLERS = new ArrayList<>();
+ public static final List> VILLAGER_REMOVE_HANDLERS = new ArrayList<>();
+ public static final List> WANDERING_TRADER_MODIFY_HANDLERS = new ArrayList<>();
+ public static final List> WANDERING_TRADER_REMOVE_HANDLERS = new ArrayList<>();
+
+ public static final Map> VILLAGER_MAX_OFFER_OVERRIDES = new HashMap<>();
+ public static Integer wanderingTraderMaxOfferOverride = null;
+
+ /**
+ * @param profession The Profession of the villager.
+ * @param level The level the villager needs. Vanilla range is 1 to 5, however mods may extend that upper limit further.
+ * @return Max offers for the villager. Returning null means no override exists
+ */
+ @Nullable
+ public static Integer getVillagerMaxOffers(VillagerProfession profession, int level) {
+ if (!VILLAGER_MAX_OFFER_OVERRIDES.containsKey(profession)) {
+ return null;
+ }
+
+ return VILLAGER_MAX_OFFER_OVERRIDES.get(profession).get(level);
+ }
+
+ /**
+ * @return Max offers for the wandering trader. Returning null means no override exists
+ */
+ @Nullable
+ public static Integer getWanderingTraderMaxOffers() {
+ return wanderingTraderMaxOfferOverride;
+ }
+
+ public static boolean invokeVillagerOfferRemoving(VillagerTradeOfferContext ctx) {
+ return VILLAGER_REMOVE_HANDLERS.stream().anyMatch(predicate -> predicate.test(ctx));
+ }
+
+ public static void invokeVillagerOfferModify(VillagerTradeOfferContext ctx) {
+ VILLAGER_MODIFY_HANDLERS.forEach(consumer -> consumer.accept(ctx));
+ }
+
+ public static boolean invokeWanderingTraderOfferRemoving(WanderingTraderOfferContext ctx) {
+ return WANDERING_TRADER_REMOVE_HANDLERS.stream().anyMatch(predicate -> predicate.test(ctx));
+ }
+
+ public static void invokeWanderingTraderOfferModify(WanderingTraderOfferContext ctx) {
+ WANDERING_TRADER_MODIFY_HANDLERS.forEach(consumer -> consumer.accept(ctx));
+ }
+}
diff --git a/common/src/main/resources/architectury-common.mixins.json b/common/src/main/resources/architectury-common.mixins.json
index cc148e31..dd768292 100644
--- a/common/src/main/resources/architectury-common.mixins.json
+++ b/common/src/main/resources/architectury-common.mixins.json
@@ -8,7 +8,10 @@
"mixins": [
"BlockLandingInvoker",
"FluidTagsAccessor",
- "MixinLightningBolt"
+ "MixinLightningBolt",
+ "AbstractVillagerMixin",
+ "VillagerMixin",
+ "WanderingTraderMixin"
],
"injectors": {
"maxShiftBy": 5,
diff --git a/common/src/main/resources/architectury.accessWidener b/common/src/main/resources/architectury.accessWidener
index 3ad2c0a1..4cadb32d 100644
--- a/common/src/main/resources/architectury.accessWidener
+++ b/common/src/main/resources/architectury.accessWidener
@@ -46,5 +46,18 @@ accessible field net/minecraft/world/item/ShovelItem FLATTENABLES Ljava/util/Map
mutable field net/minecraft/world/item/ShovelItem FLATTENABLES Ljava/util/Map;
accessible field net/minecraft/world/item/HoeItem TILLABLES Ljava/util/Map;
mutable field net/minecraft/world/item/HoeItem TILLABLES Ljava/util/Map;
+
+accessible field net/minecraft/world/item/trading/MerchantOffer baseCostA Lnet/minecraft/world/item/ItemStack;
+mutable field net/minecraft/world/item/trading/MerchantOffer baseCostA Lnet/minecraft/world/item/ItemStack;
+accessible field net/minecraft/world/item/trading/MerchantOffer costB Lnet/minecraft/world/item/ItemStack;
+mutable field net/minecraft/world/item/trading/MerchantOffer costB Lnet/minecraft/world/item/ItemStack;
+accessible field net/minecraft/world/item/trading/MerchantOffer result Lnet/minecraft/world/item/ItemStack;
+mutable field net/minecraft/world/item/trading/MerchantOffer result Lnet/minecraft/world/item/ItemStack;
+accessible field net/minecraft/world/item/trading/MerchantOffer maxUses I
+mutable field net/minecraft/world/item/trading/MerchantOffer maxUses I
+accessible field net/minecraft/world/item/trading/MerchantOffer priceMultiplier F
+accessible field net/minecraft/world/item/trading/MerchantOffer xp I
+
accessible method net/minecraft/client/renderer/item/ItemProperties registerGeneric (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ItemPropertyFunction;)Lnet/minecraft/client/renderer/item/ItemPropertyFunction;
accessible method net/minecraft/client/renderer/item/ItemProperties register (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ItemPropertyFunction;)V
+
diff --git a/fabric/src/main/resources/architectury.accessWidener b/fabric/src/main/resources/architectury.accessWidener
index a62397fd..3e7541a0 100644
--- a/fabric/src/main/resources/architectury.accessWidener
+++ b/fabric/src/main/resources/architectury.accessWidener
@@ -102,5 +102,15 @@ accessible field net/minecraft/world/item/ShovelItem FLATTENABLES Ljava/util/Map
mutable field net/minecraft/world/item/ShovelItem FLATTENABLES Ljava/util/Map;
accessible field net/minecraft/world/item/HoeItem TILLABLES Ljava/util/Map;
mutable field net/minecraft/world/item/HoeItem TILLABLES Ljava/util/Map;
+accessible field net/minecraft/world/item/trading/MerchantOffer baseCostA Lnet/minecraft/world/item/ItemStack;
+mutable field net/minecraft/world/item/trading/MerchantOffer baseCostA Lnet/minecraft/world/item/ItemStack;
+accessible field net/minecraft/world/item/trading/MerchantOffer costB Lnet/minecraft/world/item/ItemStack;
+mutable field net/minecraft/world/item/trading/MerchantOffer costB Lnet/minecraft/world/item/ItemStack;
+accessible field net/minecraft/world/item/trading/MerchantOffer result Lnet/minecraft/world/item/ItemStack;
+mutable field net/minecraft/world/item/trading/MerchantOffer result Lnet/minecraft/world/item/ItemStack;
+accessible field net/minecraft/world/item/trading/MerchantOffer maxUses I
+mutable field net/minecraft/world/item/trading/MerchantOffer maxUses I
+accessible field net/minecraft/world/item/trading/MerchantOffer priceMultiplier F
+accessible field net/minecraft/world/item/trading/MerchantOffer xp I
accessible method net/minecraft/client/renderer/item/ItemProperties registerGeneric (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ItemPropertyFunction;)Lnet/minecraft/client/renderer/item/ItemPropertyFunction;
accessible method net/minecraft/client/renderer/item/ItemProperties register (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ItemPropertyFunction;)V
diff --git a/forge/src/main/resources/META-INF/accesstransformer.cfg b/forge/src/main/resources/META-INF/accesstransformer.cfg
index 21e76b0e..14526f4f 100644
--- a/forge/src/main/resources/META-INF/accesstransformer.cfg
+++ b/forge/src/main/resources/META-INF/accesstransformer.cfg
@@ -36,4 +36,10 @@ public net.minecraft.world.storage.FolderName (Ljava/lang/String;)V
public-f net.minecraft.item.AxeItem field_203176_a # STRIPABLES
public-f net.minecraft.item.ShovelItem field_195955_e # FLATTENABLES
public-f net.minecraft.item.HoeItem field_195973_b # TILLABLES
+public-f net.minecraft.item.MerchantOffer field_222223_a # baseCostA
+public-f net.minecraft.item.MerchantOffer field_222224_b # costB
+public-f net.minecraft.item.MerchantOffer field_222225_c # result
+public-f net.minecraft.item.MerchantOffer field_222227_e # maxUses
+public net.minecraft.item.MerchantOffer field_222231_i # priceMultiplier
+public net.minecraft.item.MerchantOffer field_222232_j # xp
public net.minecraft.item.ItemModelsProperties func_239420_a_(Lnet/minecraft/util/ResourceLocation;Lnet/minecraft/item/IItemPropertyGetter;)Lnet/minecraft/item/IItemPropertyGetter; # registerGeneric
diff --git a/gradle.properties b/gradle.properties
index 0a4e6064..fa0a4442 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,7 +6,7 @@ supported_version=1.16.4/5
archives_base_name=architectury
archives_base_name_snapshot=architectury-snapshot
-base_version=1.22
+base_version=1.23
maven_group=me.shedaniel
fabric_loader_version=0.11.1
diff --git a/testmod-common/src/main/java/me/shedaniel/architectury/test/trade/TestTrades.java b/testmod-common/src/main/java/me/shedaniel/architectury/test/trade/TestTrades.java
index bb92779c..f12159bd 100644
--- a/testmod-common/src/main/java/me/shedaniel/architectury/test/trade/TestTrades.java
+++ b/testmod-common/src/main/java/me/shedaniel/architectury/test/trade/TestTrades.java
@@ -21,22 +21,99 @@ package me.shedaniel.architectury.test.trade;
import me.shedaniel.architectury.registry.trade.SimpleTrade;
import me.shedaniel.architectury.registry.trade.TradeRegistry;
+import me.shedaniel.architectury.registry.trade.VillagerTradeOfferContext;
+import me.shedaniel.architectury.registry.trade.WanderingTraderOfferContext;
import net.minecraft.core.Registry;
import net.minecraft.world.entity.npc.VillagerProfession;
import net.minecraft.world.entity.npc.VillagerTrades;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
public class TestTrades {
public static void init() {
for (VillagerProfession villagerProfession : Registry.VILLAGER_PROFESSION) {
TradeRegistry.registerVillagerTrade(villagerProfession, 1, TestTrades.createTrades());
}
TradeRegistry.registerTradeForWanderingTrader(false, TestTrades.createTrades());
+
+ TradeRegistry.modifyVillagerOffers(farmerSwitchBreadResultToGoldenApple);
+ TradeRegistry.modifyVillagerOffers(farmerCarrotsNeedSticksToo);
+ TradeRegistry.modifyVillagerOffers(farmerCarrotWithStickIncreasePriceMultiplier);
+ TradeRegistry.modifyVillagerOffers(butcherWantsManyEmeralds);
+ TradeRegistry.modifyVillagerOffers(butcherGivesMoreEmeraldForChicken);
+
+ TradeRegistry.removeVillagerOffers(removeCarrotTrade);
+ TradeRegistry.removeVillagerOffers(removeFarmersLevelTwoTrades);
+
+ TradeRegistry.setVillagerMaxOffers(VillagerProfession.FISHERMAN, 1, 100);
+ TradeRegistry.setVillagerMaxOffers(VillagerProfession.BUTCHER, 2, 100);
+
+ TradeRegistry.setVillagerMaxOffers(VillagerProfession.SHEPHERD, 1, 10); // easier to level up
+ TradeRegistry.setVillagerMaxOffers(VillagerProfession.SHEPHERD, 2, 0);
+
+ TradeRegistry.setWanderingTraderMaxOffers(7); // will end up having 8 because of the rare item
+
+ TradeRegistry.modifyWanderingTraderOffers(wanderingTraderHighRarePrice);
+ TradeRegistry.modifyWanderingTraderOffers(wanderingTraderLovesFlint);
+ TradeRegistry.removeWanderingTraderOffers(wanderingTraderRemoveDyes);
}
private static VillagerTrades.ItemListing[] createTrades() {
SimpleTrade trade = new SimpleTrade(Items.APPLE.getDefaultInstance(), ItemStack.EMPTY, Items.ACACIA_BOAT.getDefaultInstance(), 1, 0, 1.0F);
return new VillagerTrades.ItemListing[]{trade};
}
+
+ public static Consumer farmerSwitchBreadResultToGoldenApple = ctx -> {
+ if (ctx.getProfession() == VillagerProfession.FARMER && ctx.getOffer().getResult().getItem() == Items.BREAD) {
+ ctx.getOffer().setResult(new ItemStack(Items.GOLDEN_APPLE));
+ ctx.getOffer().setXp(10000); // should fill the XP bar on top of the trade gui to the moon
+ ctx.getOffer().setMaxUses(1);
+ }
+ };
+
+ public static Consumer farmerCarrotsNeedSticksToo = ctx -> {
+ if (ctx.getProfession() == VillagerProfession.FARMER && ctx.getOffer().getCostA().getItem() == Items.CARROT) {
+ ctx.getOffer().setCostB(new ItemStack(Items.STICK, 32)); // will switch the empty itemstack to 3 sticks
+ }
+ };
+
+ public static Consumer farmerCarrotWithStickIncreasePriceMultiplier = ctx -> {
+ if (ctx.getProfession() == VillagerProfession.FARMER
+ && ctx.getOffer().getCostA().getItem() == Items.CARROT
+ && ctx.getOffer().getCostB().getItem() == Items.STICK) {
+ ctx.getOffer().setPriceMultiplier(5f);
+ }
+ };
+
+ public static Consumer butcherWantsManyEmeralds = ctx -> {
+ if (ctx.getProfession() == VillagerProfession.BUTCHER && ctx.getOffer().getCostA().getItem() == Items.EMERALD) {
+ ctx.getOffer().getCostA().setCount(42);
+ }
+ };
+
+ public static Consumer butcherGivesMoreEmeraldForChicken = ctx -> {
+ if (ctx.getProfession() == VillagerProfession.BUTCHER && ctx.getOffer().getCostA().getItem() == Items.CHICKEN) {
+ ctx.getOffer().getResult().setCount(64);
+ }
+ };
+
+ public static Predicate removeCarrotTrade = ctx -> ctx.getProfession() == VillagerProfession.FARMER && ctx.getOffer().getCostA().getItem() == Items.POTATO;
+
+ public static Predicate removeFarmersLevelTwoTrades = ctx -> ctx.getProfession() == VillagerProfession.FARMER && ctx.getLevel() == 2;
+
+ public static Consumer wanderingTraderHighRarePrice = ctx -> {
+ if (ctx.isRare()) {
+ ctx.getOffer().getCostA().setCount(37);
+ }
+ };
+
+ public static Consumer wanderingTraderLovesFlint = ctx -> {
+ int count = ctx.getOffer().getCostA().getCount();
+ ctx.getOffer().setCostA(new ItemStack(Items.FLINT, count));
+ };
+
+ public static Predicate wanderingTraderRemoveDyes = ctx -> ctx.getOffer().getResult().getItem().toString().matches("^.*dye$");
}