From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Thu, 7 Oct 2021 14:34:55 -0700
Subject: [PATCH] Custom Potion Mixes

== AT ==
public-f net.minecraft.server.MinecraftServer potionBrewing

diff --git a/src/main/java/io/papermc/paper/potion/PaperPotionBrewer.java b/src/main/java/io/papermc/paper/potion/PaperPotionBrewer.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9390227a2bba4e03aa9ee592ca157127633c41b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/potion/PaperPotionBrewer.java
@@ -0,0 +1,56 @@
+package io.papermc.paper.potion;
+
+import com.google.common.base.Preconditions;
+import java.util.Collection;
+import net.minecraft.server.MinecraftServer;
+import org.bukkit.NamespacedKey;
+import org.bukkit.potion.PotionBrewer;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionType;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
+public class PaperPotionBrewer implements PotionBrewer {
+
+    private final MinecraftServer minecraftServer;
+
+    public PaperPotionBrewer(final MinecraftServer minecraftServer) {
+        this.minecraftServer = minecraftServer;
+    }
+
+    @Override
+    @Deprecated(forRemoval = true)
+    public Collection<PotionEffect> getEffects(PotionType type, boolean upgraded, boolean extended) {
+        final org.bukkit.NamespacedKey key = type.getKey();
+
+        Preconditions.checkArgument(!key.getKey().startsWith("strong_"), "Strong potion type cannot be used directly, got %s", key);
+        Preconditions.checkArgument(!key.getKey().startsWith("long_"), "Extended potion type cannot be used directly, got %s", key);
+
+        org.bukkit.NamespacedKey effectiveKey = key;
+        if (upgraded) {
+            effectiveKey = new org.bukkit.NamespacedKey(key.namespace(), "strong_" + key.key());
+        } else if (extended) {
+            effectiveKey = new org.bukkit.NamespacedKey(key.namespace(), "long_" + key.key());
+        }
+
+        final org.bukkit.potion.PotionType effectivePotionType = org.bukkit.Registry.POTION.get(effectiveKey);
+        Preconditions.checkNotNull(type, "Unknown potion type from data " + effectiveKey.asMinimalString()); // Legacy error message in 1.20.4
+        return effectivePotionType.getPotionEffects();
+    }
+
+    @Override
+    public void addPotionMix(final PotionMix potionMix) {
+        this.minecraftServer.potionBrewing().addPotionMix(potionMix);
+    }
+
+    @Override
+    public void removePotionMix(final NamespacedKey key) {
+        this.minecraftServer.potionBrewing.removePotionMix(key);
+    }
+
+    @Override
+    public void resetPotionMixes() {
+        this.minecraftServer.potionBrewing = this.minecraftServer.potionBrewing().reload(this.minecraftServer.getWorldData().enabledFeatures());
+    }
+}
diff --git a/src/main/java/io/papermc/paper/potion/PaperPotionMix.java b/src/main/java/io/papermc/paper/potion/PaperPotionMix.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ea357ac2f3a93db4ebdf24b5072be7d1cad3e33
--- /dev/null
+++ b/src/main/java/io/papermc/paper/potion/PaperPotionMix.java
@@ -0,0 +1,21 @@
+package io.papermc.paper.potion;
+
+import java.util.function.Predicate;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.inventory.CraftRecipe;
+import org.bukkit.inventory.RecipeChoice;
+
+public record PaperPotionMix(ItemStack result, Predicate<ItemStack> input, Predicate<ItemStack> ingredient) {
+
+    public PaperPotionMix(PotionMix potionMix) {
+        this(CraftItemStack.asNMSCopy(potionMix.getResult()), convert(potionMix.getInput()), convert(potionMix.getIngredient()));
+    }
+
+    static Predicate<ItemStack> convert(final RecipeChoice choice) {
+        if (choice instanceof PredicateRecipeChoice predicateRecipeChoice) {
+            return stack -> predicateRecipeChoice.test(CraftItemStack.asBukkitCopy(stack));
+        }
+        return CraftRecipe.toIngredient(choice, true);
+    }
+}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 03ee72a20330dd9ba6ff2808c1252454a6c217bc..fcc00d5b0be3d2adb92c8243ccca8d0190fcc413 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -2196,6 +2196,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
             this.worldData.setDataConfiguration(worlddataconfiguration);
             this.resources.managers.updateStaticRegistryTags();
             this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures());
+            this.potionBrewing = this.potionBrewing.reload(this.worldData.enabledFeatures()); // Paper - Custom Potion Mixes
             this.getPlayerList().saveAll();
             this.getPlayerList().reloadResources();
             this.functionManager.replaceLibrary(this.resources.managers.getFunctionLibrary());
diff --git a/src/main/java/net/minecraft/world/inventory/BrewingStandMenu.java b/src/main/java/net/minecraft/world/inventory/BrewingStandMenu.java
index 6acc06ab49549ebd80e9871dce7f3fe6ebde9498..d2688ca4430e2e45b35b97b0b3b7c79b5aac23a4 100644
--- a/src/main/java/net/minecraft/world/inventory/BrewingStandMenu.java
+++ b/src/main/java/net/minecraft/world/inventory/BrewingStandMenu.java
@@ -57,9 +57,11 @@ public class BrewingStandMenu extends AbstractContainerMenu {
         this.brewingStandData = propertyDelegate;
         PotionBrewing potionbrewer = playerInventory.player.level().potionBrewing();
 
-        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 0, 56, 51));
-        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 1, 79, 58));
-        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 2, 102, 51));
+        // Paper start - custom potion mixes
+        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 0, 56, 51, potionbrewer));
+        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 1, 79, 58, potionbrewer));
+        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 2, 102, 51, potionbrewer));
+        // Paper end - custom potion mixes
         this.ingredientSlot = this.addSlot(new BrewingStandMenu.IngredientsSlot(potionbrewer, inventory, 3, 79, 17));
         this.addSlot(new BrewingStandMenu.FuelSlot(inventory, 4, 17, 17));
         this.addDataSlots(propertyDelegate);
@@ -90,7 +92,7 @@ public class BrewingStandMenu extends AbstractContainerMenu {
                     if (!this.moveItemStackTo(itemstack1, 3, 4, false)) {
                         return ItemStack.EMPTY;
                     }
-                } else if (BrewingStandMenu.PotionSlot.mayPlaceItem(itemstack)) {
+                } else if (BrewingStandMenu.PotionSlot.mayPlaceItem(itemstack, this.player.player.level().potionBrewing())) { // Paper - custom potion mixes
                     if (!this.moveItemStackTo(itemstack1, 0, 3, false)) {
                         return ItemStack.EMPTY;
                     }
@@ -139,13 +141,15 @@ public class BrewingStandMenu extends AbstractContainerMenu {
 
     private static class PotionSlot extends Slot {
 
-        public PotionSlot(Container inventory, int index, int x, int y) {
+        private final PotionBrewing potionBrewing; // Paper - custom potion mixes
+        public PotionSlot(Container inventory, int index, int x, int y, PotionBrewing potionBrewing) { // Paper - custom potion mixes
             super(inventory, index, x, y);
+            this.potionBrewing = potionBrewing; // Paper - custom potion mixes
         }
 
         @Override
         public boolean mayPlace(ItemStack stack) {
-            return PotionSlot.mayPlaceItem(stack);
+            return PotionSlot.mayPlaceItem(stack, this.potionBrewing); // Paper - custom potion mixes
         }
 
         @Override
@@ -164,8 +168,8 @@ public class BrewingStandMenu extends AbstractContainerMenu {
             super.onTake(player, stack);
         }
 
-        public static boolean mayPlaceItem(ItemStack stack) {
-            return stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE);
+        public static boolean mayPlaceItem(ItemStack stack, PotionBrewing potionBrewing) { // Paper - custom potion mixes
+            return stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE) || potionBrewing.isCustomInput(stack); // Paper - Custom Potion Mixes
         }
 
         @Override
diff --git a/src/main/java/net/minecraft/world/item/alchemy/PotionBrewing.java b/src/main/java/net/minecraft/world/item/alchemy/PotionBrewing.java
index 50e81e3babd331077eda8daa769eb2b3f99e8ca2..ca01f3344005b295c7ae98f6d5b03f79513b12a4 100644
--- a/src/main/java/net/minecraft/world/item/alchemy/PotionBrewing.java
+++ b/src/main/java/net/minecraft/world/item/alchemy/PotionBrewing.java
@@ -19,6 +19,7 @@ public class PotionBrewing {
     private final List<Ingredient> containers;
     private final List<PotionBrewing.Mix<Potion>> potionMixes;
     private final List<PotionBrewing.Mix<Item>> containerMixes;
+    private final it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<org.bukkit.NamespacedKey, io.papermc.paper.potion.PaperPotionMix> customMixes = new it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<>(); // Paper - Custom Potion Mixes
 
     PotionBrewing(List<Ingredient> potionTypes, List<PotionBrewing.Mix<Potion>> potionRecipes, List<PotionBrewing.Mix<Item>> itemRecipes) {
         this.containers = potionTypes;
@@ -27,7 +28,7 @@ public class PotionBrewing {
     }
 
     public boolean isIngredient(ItemStack stack) {
-        return this.isContainerIngredient(stack) || this.isPotionIngredient(stack);
+        return this.isContainerIngredient(stack) || this.isPotionIngredient(stack) || this.isCustomIngredient(stack); // Paper - Custom Potion Mixes
     }
 
     private boolean isContainer(ItemStack stack) {
@@ -71,6 +72,11 @@ public class PotionBrewing {
     }
 
     public boolean hasMix(ItemStack input, ItemStack ingredient) {
+        // Paper start - Custom Potion Mixes
+        if (this.hasCustomMix(input, ingredient)) {
+            return true;
+        }
+        // Paper end - Custom Potion Mixes
         return this.isContainer(input) && (this.hasContainerMix(input, ingredient) || this.hasPotionMix(input, ingredient));
     }
 
@@ -103,6 +109,13 @@ public class PotionBrewing {
         if (input.isEmpty()) {
             return input;
         } else {
+            // Paper start - Custom Potion Mixes
+            for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
+                if (mix.input().test(input) && mix.ingredient().test(ingredient)) {
+                    return mix.result().copy();
+                }
+            }
+            // Paper end - Custom Potion Mixes
             Optional<Holder<Potion>> optional = input.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY).potion();
             if (optional.isEmpty()) {
                 return input;
@@ -190,6 +203,50 @@ public class PotionBrewing {
         builder.addMix(Potions.SLOW_FALLING, Items.REDSTONE, Potions.LONG_SLOW_FALLING);
     }
 
+    // Paper start - Custom Potion Mixes
+    public boolean isCustomIngredient(ItemStack stack) {
+        for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
+            if (mix.ingredient().test(stack)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean isCustomInput(ItemStack stack) {
+        for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
+            if (mix.input().test(stack)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasCustomMix(ItemStack input, ItemStack ingredient) {
+        for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
+            if (mix.input().test(input) && mix.ingredient().test(ingredient)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void addPotionMix(io.papermc.paper.potion.PotionMix mix) {
+        if (this.customMixes.containsKey(mix.getKey())) {
+            throw new IllegalArgumentException("Duplicate recipe ignored with ID " + mix.getKey());
+        }
+        this.customMixes.putAndMoveToFirst(mix.getKey(), new io.papermc.paper.potion.PaperPotionMix(mix));
+    }
+
+    public boolean removePotionMix(org.bukkit.NamespacedKey key) {
+        return this.customMixes.remove(key) != null;
+    }
+
+    public PotionBrewing reload(FeatureFlagSet flags) {
+        return bootstrap(flags);
+    }
+    // Paper end - Custom Potion Mixes
+
     public static class Builder {
         private final List<Ingredient> containers = new ArrayList<>();
         private final List<PotionBrewing.Mix<Potion>> potionMixes = new ArrayList<>();
diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
index e167c2834f1b7899a7d11cef782940deeb739a9c..2bafacd7bc56186d9105d2031180f8c4a6940018 100644
--- a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
+++ b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
@@ -316,12 +316,12 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements
 
     @Override
     public boolean canPlaceItem(int slot, ItemStack stack) {
+        PotionBrewing potionbrewer = this.level != null ? this.level.potionBrewing() : PotionBrewing.EMPTY; // Paper - move up
         if (slot == 3) {
-            PotionBrewing potionbrewer = this.level != null ? this.level.potionBrewing() : PotionBrewing.EMPTY;
 
             return potionbrewer.isIngredient(stack);
         } else {
-            return slot == 4 ? stack.is(ItemTags.BREWING_FUEL) : (stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE)) && this.getItem(slot).isEmpty();
+            return slot == 4 ? stack.is(ItemTags.BREWING_FUEL) : (stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE) || potionbrewer.isCustomInput(stack)) && this.getItem(slot).isEmpty(); // Paper - Custom Potion Mixes
         }
     }
 
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 75bd9617164c63a641602bf772a5e0f15a322c7e..82e1c4713e043c4903b1f0154609da4558f90aef 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -311,6 +311,7 @@ public final class CraftServer implements Server {
     private final io.papermc.paper.datapack.PaperDatapackManager datapackManager; // Paper
     public static Exception excessiveVelEx; // Paper - Velocity warnings
     private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper
+    private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes
 
     static {
         ConfigurationSerialization.registerClass(CraftOfflinePlayer.class);
@@ -394,6 +395,7 @@ public final class CraftServer implements Server {
         if (this.configuration.getBoolean("settings.use-map-color-cache")) {
             MapPalette.setMapColorCache(new CraftMapColorCache(this.logger));
         }
+        this.potionBrewer = new io.papermc.paper.potion.PaperPotionBrewer(console); // Paper - custom potion mixes
         datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper
     }
 
@@ -3076,5 +3078,9 @@ public final class CraftServer implements Server {
         return datapackManager;
     }
 
+    @Override
+    public io.papermc.paper.potion.PaperPotionBrewer getPotionBrewer() {
+        return this.potionBrewer;
+    }
     // Paper end
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java
index fa47232eeb36ed92b5a526bb00398f08b6c0ab41..5b176cd5f80a49e3a2afbcbf4fe2958143719bce 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java
@@ -23,6 +23,11 @@ public interface CraftRecipe extends Recipe {
     }
 
     default Ingredient toNMS(RecipeChoice bukkit, boolean requireNotEmpty) {
+        // Paper start
+        return toIngredient(bukkit, requireNotEmpty);
+    }
+    static Ingredient toIngredient(RecipeChoice bukkit, boolean requireNotEmpty) {
+        // Paper end
         Ingredient stack;
 
         if (bukkit == null) {