瀏覽代碼

Feature/villager trades (#122)

* Add modify and removing for villager trades

- Add mixin for villager trades
- Add methods to register modify and removing
- Implement base for VillagerMixin to provide additional villager data
- Basic Access & Mixin change
- Add AT and AW
- Add overriding for max offers a villager or the wanderer can have

* Add rare check for wandering trader

* Remove todo comment

* rename some methods

* Solve reviews for #122

Move non api stuff into TradeRegistryData
Rename fields in MerchantOfferAccess
Move trade stuff into internal package
Mark internal trade classes as ApiStatus.Internal

* Minor refactors (discussed on Discord)

* Add doc for AbstractVillagerMixin

* Reformat code

* Update gradle.properties

Co-authored-by: Max <maxh2709@gmail.com>
lythowastaken 3 年之前
父節點
當前提交
0fcbf40c7f
共有 18 個文件被更改,包括 799 次插入4 次删除
  1. 1 1
      build.gradle
  2. 102 0
      common/src/main/java/me/shedaniel/architectury/mixin/AbstractVillagerMixin.java
  3. 63 0
      common/src/main/java/me/shedaniel/architectury/mixin/VillagerMixin.java
  4. 89 0
      common/src/main/java/me/shedaniel/architectury/mixin/WanderingTraderMixin.java
  5. 83 0
      common/src/main/java/me/shedaniel/architectury/registry/trade/MerchantOfferAccess.java
  6. 1 1
      common/src/main/java/me/shedaniel/architectury/registry/trade/SimpleTrade.java
  7. 49 0
      common/src/main/java/me/shedaniel/architectury/registry/trade/TradeOfferContext.java
  8. 84 0
      common/src/main/java/me/shedaniel/architectury/registry/trade/TradeRegistry.java
  9. 56 0
      common/src/main/java/me/shedaniel/architectury/registry/trade/VillagerTradeOfferContext.java
  10. 21 0
      common/src/main/java/me/shedaniel/architectury/registry/trade/WanderingTraderOfferContext.java
  11. 76 0
      common/src/main/java/me/shedaniel/architectury/registry/trade/impl/OfferMixingContext.java
  12. 63 0
      common/src/main/java/me/shedaniel/architectury/registry/trade/impl/TradeRegistryData.java
  13. 4 1
      common/src/main/resources/architectury-common.mixins.json
  14. 13 0
      common/src/main/resources/architectury.accessWidener
  15. 10 0
      fabric/src/main/resources/architectury.accessWidener
  16. 6 0
      forge/src/main/resources/META-INF/accesstransformer.cfg
  17. 1 1
      gradle.properties
  18. 77 0
      testmod-common/src/main/java/me/shedaniel/architectury/test/trade/TestTrades.java

+ 1 - 1
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"

+ 102 - 0
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.
+ * <p>
+ * 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.
+ * <p>
+ * 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<OfferMixingContext> 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<Integer> overrideIterator(Set<Integer> 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;
+    }
+}

+ 63 - 0
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());
+    }
+}

+ 89 - 0
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<VillagerTrades.ItemListing> 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();
+    }
+}

+ 83 - 0
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;
+    }
+}

+ 1 - 1
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.

+ 49 - 0
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;
+    }
+}

+ 84 - 0
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<Integer, Integer> 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<VillagerTradeOfferContext> 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<VillagerTradeOfferContext> 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<WanderingTraderOfferContext> 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<WanderingTraderOfferContext> 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;
+    }
 }

+ 56 - 0
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;
+    }
+}

+ 21 - 0
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;
+    }
+}

+ 76 - 0
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<Integer> 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<Integer> shuffled = createShuffledIndexList();
+        this.iterator = shuffled.iterator();
+    }
+    
+    public void skipIteratorIfMaxOffersReached() {
+        currentIndex++;
+        if (currentIndex >= getMaxOffers()) {
+            skip();
+        }
+    }
+    
+    @NotNull
+    public Iterator<Integer> getIterator() {
+        return iterator;
+    }
+    
+    private void skip() {
+        iterator.forEachRemaining(($) -> {
+        });
+    }
+    
+    @NotNull
+    private List<Integer> createShuffledIndexList() {
+        List<Integer> 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;
+    }
+}

+ 63 - 0
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<Consumer<VillagerTradeOfferContext>> VILLAGER_MODIFY_HANDLERS = new ArrayList<>();
+    public static final List<Predicate<VillagerTradeOfferContext>> VILLAGER_REMOVE_HANDLERS = new ArrayList<>();
+    public static final List<Consumer<WanderingTraderOfferContext>> WANDERING_TRADER_MODIFY_HANDLERS = new ArrayList<>();
+    public static final List<Predicate<WanderingTraderOfferContext>> WANDERING_TRADER_REMOVE_HANDLERS = new ArrayList<>();
+    
+    public static final Map<VillagerProfession, Map<Integer, Integer>> 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));
+    }
+}

+ 4 - 1
common/src/main/resources/architectury-common.mixins.json

@@ -8,7 +8,10 @@
   "mixins": [
     "BlockLandingInvoker",
     "FluidTagsAccessor",
-    "MixinLightningBolt"
+    "MixinLightningBolt",
+    "AbstractVillagerMixin",
+    "VillagerMixin",
+    "WanderingTraderMixin"
   ],
   "injectors": {
     "maxShiftBy": 5,

+ 13 - 0
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
+

+ 10 - 0
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

+ 6 - 0
forge/src/main/resources/META-INF/accesstransformer.cfg

@@ -36,4 +36,10 @@ public net.minecraft.world.storage.FolderName <init>(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

+ 1 - 1
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

+ 77 - 0
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<VillagerTradeOfferContext> 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<VillagerTradeOfferContext> 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<VillagerTradeOfferContext> 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<VillagerTradeOfferContext> butcherWantsManyEmeralds = ctx -> {
+        if (ctx.getProfession() == VillagerProfession.BUTCHER && ctx.getOffer().getCostA().getItem() == Items.EMERALD) {
+            ctx.getOffer().getCostA().setCount(42);
+        }
+    };
+    
+    public static Consumer<VillagerTradeOfferContext> butcherGivesMoreEmeraldForChicken = ctx -> {
+        if (ctx.getProfession() == VillagerProfession.BUTCHER && ctx.getOffer().getCostA().getItem() == Items.CHICKEN) {
+            ctx.getOffer().getResult().setCount(64);
+        }
+    };
+    
+    public static Predicate<VillagerTradeOfferContext> removeCarrotTrade = ctx -> ctx.getProfession() == VillagerProfession.FARMER && ctx.getOffer().getCostA().getItem() == Items.POTATO;
+    
+    public static Predicate<VillagerTradeOfferContext> removeFarmersLevelTwoTrades = ctx -> ctx.getProfession() == VillagerProfession.FARMER && ctx.getLevel() == 2;
+    
+    public static Consumer<WanderingTraderOfferContext> wanderingTraderHighRarePrice = ctx -> {
+        if (ctx.isRare()) {
+            ctx.getOffer().getCostA().setCount(37);
+        }
+    };
+    
+    public static Consumer<WanderingTraderOfferContext> wanderingTraderLovesFlint = ctx -> {
+        int count = ctx.getOffer().getCostA().getCount();
+        ctx.getOffer().setCostA(new ItemStack(Items.FLINT, count));
+    };
+    
+    public static Predicate<WanderingTraderOfferContext> wanderingTraderRemoveDyes = ctx -> ctx.getOffer().getResult().getItem().toString().matches("^.*dye$");
 }