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