diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/entities/merchant/CustomBukkitMerchant.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/entities/merchant/CustomBukkitMerchant.java new file mode 100644 index 000000000..de4f86176 --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/entities/merchant/CustomBukkitMerchant.java @@ -0,0 +1,61 @@ +package vg.civcraft.mc.civmodcore.entities.merchant; + +import io.papermc.paper.event.player.PlayerTradeEvent; +import java.lang.reflect.Field; +import java.util.logging.Level; +import net.kyori.adventure.text.Component; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMerchant; +import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMerchantCustom; +import vg.civcraft.mc.civmodcore.util.CivLogger; + +/** + * This is an alternative to {@link Bukkit#createMerchant(Component)} that re-adds + * {@link PlayerTradeEvent} emissions. Just do: {@code new CustomBukkitMerchant(Component)} + */ +public class CustomBukkitMerchant extends CraftMerchantCustom { + + private static final CivLogger LOGGER; + private static final Field MERCHANT_FIELD; + + static { + LOGGER = CivLogger.getLogger(CustomBukkitMerchant.class); + MERCHANT_FIELD = FieldUtils.getField(CraftMerchant.class, "merchant", true); + FieldUtils.removeFinalModifier(MERCHANT_FIELD); + } + + public CustomBukkitMerchant(final Component title) { + super(title); + final var nmsMerchant = new CustomNMSMerchant(this, title); + try { + FieldUtils.writeField(MERCHANT_FIELD, this, nmsMerchant, true); + } + catch (final IllegalAccessException exception) { + LOGGER.log(Level.SEVERE, + "Could not re-set merchant to [" + nmsMerchant + "]", + exception); + } + } + + public CustomNMSMerchant getNMSMerchant() { + return (CustomNMSMerchant) super.getMerchant(); + } + + /** + * Use {@link #getNMSMerchant()} instead. The only reason why this is being kept is because + * the super constructor calls this function and expects this type, not {@link CustomNMSMerchant}. + */ + @Deprecated + @Override + public CraftMerchantCustom.MinecraftMerchant getMerchant() { + return super.getMerchant(); + } + + @Override + public String toString() { + /** Stolen from {@link Object#toString()} because the super version is garbage */ + return getClass().getName() + "@" + Integer.toHexString(hashCode()); + } + +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/entities/merchant/CustomNMSMerchant.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/entities/merchant/CustomNMSMerchant.java new file mode 100644 index 000000000..0d5347da3 --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/entities/merchant/CustomNMSMerchant.java @@ -0,0 +1,80 @@ +package vg.civcraft.mc.civmodcore.entities.merchant; + +import io.papermc.paper.event.player.PlayerTradeEvent; +import java.lang.reflect.Field; +import java.util.Objects; +import net.kyori.adventure.text.Component; +import net.minecraft.server.v1_16_R3.Entity; +import net.minecraft.server.v1_16_R3.EntityExperienceOrb; +import net.minecraft.server.v1_16_R3.EntityPlayer; +import net.minecraft.server.v1_16_R3.EntityVillagerAbstract; +import net.minecraft.server.v1_16_R3.EntityVillagerTrader; +import net.minecraft.server.v1_16_R3.MerchantRecipe; +import net.minecraft.server.v1_16_R3.MerchantRecipeList; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMerchantCustom; +import org.bukkit.entity.AbstractVillager; +import org.bukkit.entity.ExperienceOrb; + +public class CustomNMSMerchant extends CraftMerchantCustom.MinecraftMerchant { + + private static final Field TRADES_FIELD; + + static { + TRADES_FIELD = FieldUtils.getField(CraftMerchantCustom.MinecraftMerchant.class, "trades", true); + FieldUtils.removeFinalModifier(TRADES_FIELD); + } + + CustomNMSMerchant(final CustomBukkitMerchant merchant, final Component title) { + super(title); + this.craftMerchant = Objects.requireNonNull(merchant); + } + + /** + * @return Returns the raw offers object (which is mutable) via reflection. + */ + public MerchantRecipeList getRawOffers() { + try { + return (MerchantRecipeList) FieldUtils.readField(TRADES_FIELD, this, true); + } + catch (final IllegalAccessException exception) { + throw new RuntimeException(exception); + } + } + + /** + * This is based heavily on {@link EntityVillagerAbstract#a(MerchantRecipe)}. This method is called + * by NMS when a trade is being purchased. + */ + @Override + public void a(final MerchantRecipe trade) { + if (getTrader() instanceof EntityPlayer) { + final var trader = (EntityPlayer) getTrader(); + final var event = new PlayerTradeEvent( + trader.getBukkitEntity(), + (AbstractVillager) getCraftMerchant(), + trade.asBukkit(), + true, // reward xp? + true); // should increase uses? + event.callEvent(); + if (event.isCancelled()) { + return; + } + final var eventTrade = event.getTrade(); + if (event.willIncreaseTradeUses()) { + eventTrade.setUses(eventTrade.getUses() + 1); + } + if (event.isRewardingExp() && eventTrade.hasExperienceReward()) { + /** Based on {@link EntityVillagerTrader#b(MerchantRecipe)} */ + final int xp = 3 + Entity.SHARED_RANDOM.nextInt(4); + final var world = trader.getWorld(); + world.addEntity(new EntityExperienceOrb( + world, trader.locX(), trader.locY() + 0.5d, trader.locZ(), + xp, ExperienceOrb.SpawnReason.VILLAGER_TRADE, trader, null)); + } + return; + } + super.a(trade); + } + +}