diff --git a/patches/api/0002-Purpur-API-Changes.patch b/patches/api/0002-Purpur-API-Changes.patch new file mode 100644 index 00000000..2ee331bd --- /dev/null +++ b/patches/api/0002-Purpur-API-Changes.patch @@ -0,0 +1,4096 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Github Actions +Date: Tue, 23 May 2023 21:25:15 +0000 +Subject: [PATCH] Purpur API Changes + +Original license: MIT +Original project: https://github.com/PurpurMC/Purpur + +diff --git a/build.gradle.kts b/build.gradle.kts +index e23ed86c4288ecdfef37bf5b2cd132f348bf852a..3e148458cc78a3225e8d6572b43e1d358791eec2 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -108,6 +108,8 @@ tasks.jar { + } + + tasks.withType { ++ (options as StandardJavadocDocletOptions).addStringOption("-add-modules", "jdk.incubator.vector") // Purpur - our javadocs need this for pufferfish's SIMD patch ++ (options as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet") // Purpur - silence Paper's bajillion javadoc warnings + val options = options as StandardJavadocDocletOptions + options.overview = "src/main/javadoc/overview.html" + options.use() +diff --git a/src/main/java/co/aikar/timings/TimedEventExecutor.java b/src/main/java/co/aikar/timings/TimedEventExecutor.java +index 8f29c1561ba5916cb5634392edd8bd2a5a294a51..6fbc64e0f214d0c8e5afcbe385e414a4e1fe1c72 100644 +--- a/src/main/java/co/aikar/timings/TimedEventExecutor.java ++++ b/src/main/java/co/aikar/timings/TimedEventExecutor.java +@@ -77,9 +77,9 @@ public class TimedEventExecutor implements EventExecutor { + executor.execute(listener, event); + return; + } +- try (Timing ignored = timings.startTiming()){ ++ //try (Timing ignored = timings.startTiming()){ // Purpur + executor.execute(listener, event); +- } ++ //} // Purpur + } + + @Override +diff --git a/src/main/java/co/aikar/timings/Timing.java b/src/main/java/co/aikar/timings/Timing.java +index 7514fad26f955329f8bf17ff17db75f0c8301ee5..1d866e980abc542bdfee1ce082cd9cdd7761e9f7 100644 +--- a/src/main/java/co/aikar/timings/Timing.java ++++ b/src/main/java/co/aikar/timings/Timing.java +@@ -39,6 +39,7 @@ public interface Timing extends AutoCloseable { + * @return Timing + */ + @NotNull ++ @io.papermc.paper.annotation.DoNotUse // Purpur + Timing startTiming(); + + /** +@@ -46,6 +47,7 @@ public interface Timing extends AutoCloseable { + * + * Will automatically be called when this Timing is used with try-with-resources + */ ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void stopTiming(); + + /** +@@ -56,6 +58,7 @@ public interface Timing extends AutoCloseable { + * @return Timing + */ + @NotNull ++ @io.papermc.paper.annotation.DoNotUse // Purpur + Timing startTimingIfSync(); + + /** +@@ -65,12 +68,14 @@ public interface Timing extends AutoCloseable { + * + * But only if we are on the primary thread. + */ ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void stopTimingIfSync(); + + /** + * @deprecated Doesn't do anything - Removed + */ + @Deprecated ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void abort(); + + /** +@@ -82,5 +87,6 @@ public interface Timing extends AutoCloseable { + TimingHandler getTimingHandler(); + + @Override ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void close(); + } +diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java +index 9812d668ad945aba486fbf6d5bf83c4292cb5d03..752d54830aa8baa1450bf72da03ae55ed30293c2 100644 +--- a/src/main/java/co/aikar/timings/Timings.java ++++ b/src/main/java/co/aikar/timings/Timings.java +@@ -124,7 +124,7 @@ public final class Timings { + @NotNull + public static Timing ofStart(@NotNull Plugin plugin, @NotNull String name, @Nullable Timing groupHandler) { + Timing timing = of(plugin, name, groupHandler); +- timing.startTiming(); ++ //timing.startTiming(); // Purpur + return timing; + } + +@@ -145,9 +145,11 @@ public final class Timings { + * @param enabled Should timings be reported + */ + public static void setTimingsEnabled(boolean enabled) { +- timingsEnabled = enabled; +- warnAboutDeprecationOnEnable(); +- reset(); ++ // Purpur start - we don't do that here... ++ timingsEnabled = false; ++ //warnAboutDeprecationOnEnable(); ++ //reset(); ++ // Purpur end + } + + private static void warnAboutDeprecationOnEnable() { +diff --git a/src/main/java/co/aikar/timings/TimingsCommand.java b/src/main/java/co/aikar/timings/TimingsCommand.java +index e801e79fa57c44b2e5d359647c920f88064826f1..1abfcee0f6d632f4cd8d74b4994a90c9ea9d254c 100644 +--- a/src/main/java/co/aikar/timings/TimingsCommand.java ++++ b/src/main/java/co/aikar/timings/TimingsCommand.java +@@ -45,7 +45,7 @@ public class TimingsCommand extends BukkitCommand { + public TimingsCommand(@NotNull String name) { + super(name); + this.description = "Manages Spigot Timings data to see performance of the server."; +- this.usageMessage = "/timings "; ++ this.usageMessage = "/timings";// "; // Purpur + this.setPermission("bukkit.command.timings"); + } + +@@ -54,8 +54,12 @@ public class TimingsCommand extends BukkitCommand { + if (!testPermission(sender)) { + return true; + } +- if (false) { +- sender.sendMessage(Timings.deprecationMessage()); ++ if (true) { ++ net.kyori.adventure.text.minimessage.MiniMessage mm = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage(); ++ sender.sendMessage(mm.deserialize("Purpur has removed timings to save your performance. Please use /spark instead")); ++ sender.sendMessage(mm.deserialize("For more information, view its documentation at")); ++ sender.sendMessage(mm.deserialize("https://spark.lucko.me/docs/Command-Usage")); ++ return true; + } + if (args.length < 1) { + sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED)); +@@ -115,7 +119,7 @@ public class TimingsCommand extends BukkitCommand { + Preconditions.checkNotNull(args, "Arguments cannot be null"); + Preconditions.checkNotNull(alias, "Alias cannot be null"); + +- if (args.length == 1) { ++ if (false && args.length == 1) { // Purpur + return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, + new ArrayList(TIMINGS_SUBCOMMANDS.size())); + } +diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java b/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java +index 998f629852e1103767e005405d1f39c2251ecd28..3c05b03bb5ff3bfec6c69a5cc4b23f0633ab473f 100644 +--- a/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java ++++ b/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java +@@ -200,6 +200,18 @@ public interface VanillaGoal extends Goal { + GoalKey CLIMB_ON_TOP_OF_POWDER_SNOW = GoalKey.of(Mob.class, NamespacedKey.minecraft("climb_on_top_of_powder_snow")); + GoalKey WOLF_PANIC = GoalKey.of(Wolf.class, NamespacedKey.minecraft("wolf_panic")); + ++ // Purpur start ++ GoalKey MOB_HAS_RIDER = GoalKey.of(Mob.class, NamespacedKey.minecraft("has_rider")); ++ GoalKey HORSE_HAS_RIDER = GoalKey.of(AbstractHorse.class, NamespacedKey.minecraft("horse_has_rider")); ++ GoalKey LLAMA_HAS_RIDER = GoalKey.of(Llama.class, NamespacedKey.minecraft("llama_has_rider")); ++ GoalKey FIND_CRYSTAL = GoalKey.of(Phantom.class, NamespacedKey.minecraft("find_crystal")); ++ GoalKey ORBIT_CRYSTAL = GoalKey.of(Phantom.class, NamespacedKey.minecraft("orbit_crystal")); ++ GoalKey DROWNED_ATTACK_VILLAGER = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_attack_villager")); ++ GoalKey ZOMBIE_ATTACK_VILLAGER = GoalKey.of(Zombie.class, NamespacedKey.minecraft("zombie_attack_villager")); ++ GoalKey AVOID_RABID_WOLF = GoalKey.of(Wolf.class, NamespacedKey.minecraft("avoid_rabid_wolf")); ++ GoalKey RECEIVE_FLOWER = GoalKey.of(IronGolem.class, NamespacedKey.minecraft("receive_flower")); ++ // Purpur end ++ + /** + * @deprecated removed in 1.16 + */ +diff --git a/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java +index a736d7bcdc5861a01b66ba36158db1c716339346..22fc165fd9c95f0f3ae1be7a0857e48cc50fad5b 100644 +--- a/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java +@@ -26,6 +26,12 @@ public interface VersionFetcher { + @NotNull + Component getVersionMessage(@NotNull String serverVersion); + ++ // Purpur start ++ default int distance() { ++ return 0; ++ } ++ // Purpur end ++ + class DummyVersionFetcher implements VersionFetcher { + + @Override +diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java +index bf33dfa921e0099490e5485ac85a0c84645929e7..ad5bee2433359f9a63546f0c06897011e014809a 100644 +--- a/src/main/java/org/bukkit/Bukkit.java ++++ b/src/main/java/org/bukkit/Bukkit.java +@@ -2514,4 +2514,127 @@ public final class Bukkit { + public static Server.Spigot spigot() { + return server.spigot(); + } ++ ++ // Purpur start ++ /** ++ * Get the name of this server ++ * @return the name of the server ++ */ ++ @NotNull ++ public static String getServerName() { ++ return server.getServerName(); ++ } ++ ++ /** ++ * Check if server is lagging according to laggy threshold setting ++ * ++ * @return True if lagging ++ */ ++ public static boolean isLagging() { ++ return server.isLagging(); ++ } ++ ++ /** ++ * Add an Item as fuel for furnaces ++ * ++ * @param material The material that will be the fuel ++ * @param burnTime The time (in ticks) this item will burn for ++ */ ++ public static void addFuel(@NotNull Material material, int burnTime) { ++ server.addFuel(material, burnTime); ++ } ++ ++ /** ++ * Remove an item as fuel for furnaces ++ * ++ * @param material The material that will no longer be a fuel ++ */ ++ public static void removeFuel(@NotNull Material material) { ++ server.removeFuel(material); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration) { ++ server.sendBlockHighlight(location, duration); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, int argb) { ++ server.sendBlockHighlight(location, duration, argb); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text) { ++ server.sendBlockHighlight(location, duration, text); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb) { ++ server.sendBlockHighlight(location, duration, text, argb); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency) { ++ server.sendBlockHighlight(location, duration, color, transparency); ++ } ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ public static void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency) { ++ server.sendBlockHighlight(location, duration, text, color, transparency); ++ } ++ ++ /** ++ * Clears all debug block highlights for all players on the server. ++ */ ++ public static void clearBlockHighlights() { ++ server.clearBlockHighlights(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/ChatColor.java b/src/main/java/org/bukkit/ChatColor.java +index ea4ceb643239c26851bacbf45fc3f2efef3bb4be..3b8395dcb73e3fb251adf7438cbc7e95c4185a3a 100644 +--- a/src/main/java/org/bukkit/ChatColor.java ++++ b/src/main/java/org/bukkit/ChatColor.java +@@ -3,6 +3,7 @@ package org.bukkit; + import com.google.common.base.Preconditions; + import com.google.common.collect.Maps; + import java.util.Map; ++import java.util.regex.Matcher; + import java.util.regex.Pattern; + import org.jetbrains.annotations.Contract; + import org.jetbrains.annotations.NotNull; +@@ -455,4 +456,77 @@ public enum ChatColor { + BY_CHAR.put(color.code, color); + } + } ++ ++ // Purpur start ++ /** ++ * Convert legacy string into a string ready to be parsed by MiniMessage ++ * ++ * @param str Legacy string ++ * @return MiniMessage ready string ++ */ ++ @NotNull ++ public static String toMM(@NotNull String str) { ++ StringBuilder sb = new StringBuilder(str); ++ Matcher m = STRIP_COLOR_PATTERN.matcher(sb); ++ while (m.find()) { ++ sb.replace(m.start(), m.end(), sb.substring(m.start(), m.end()).toLowerCase()); ++ } ++ return sb.toString() ++ .replace("&0", "") ++ .replace("&1", "") ++ .replace("&2", "") ++ .replace("&3", "") ++ .replace("&4", "") ++ .replace("&5", "") ++ .replace("&6", "") ++ .replace("&7", "") ++ .replace("&8", "") ++ .replace("&9", "") ++ .replace("&a", "") ++ .replace("&b", "") ++ .replace("&c", "") ++ .replace("&d", "") ++ .replace("&e", "") ++ .replace("&f", "") ++ .replace("&k", "") ++ .replace("&l", "") ++ .replace("&m", "") ++ .replace("&n", "") ++ .replace("&o", "") ++ .replace("&r", ""); ++ } ++ ++ @NotNull ++ public static net.kyori.adventure.text.Component parseMM(@NotNull String string, @Nullable Object... args) { ++ return net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(String.format(string, args)); ++ } ++ ++ @Deprecated ++ public static final Pattern HEX_PATTERN = Pattern.compile("(#[A-Fa-f0-9]{6})"); ++ ++ @Deprecated ++ @Nullable ++ public static String replaceHex(@Nullable String str) { ++ if (str != null) { ++ java.util.regex.Matcher matcher = HEX_PATTERN.matcher(str); ++ while (matcher.find()) { ++ String group = matcher.group(1); ++ str = str.replace(group, net.md_5.bungee.api.ChatColor.of(group).toString()); ++ } ++ } ++ return str; ++ } ++ ++ @Deprecated ++ @Nullable ++ public static String color(@Nullable String str) { ++ return color(str, true); ++ } ++ ++ @Deprecated ++ @Nullable ++ public static String color(@Nullable String str, boolean parseHex) { ++ return str != null ? net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', parseHex ? replaceHex(str) : str) : str; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/Material.java b/src/main/java/org/bukkit/Material.java +index adb2416887cc3b544ec36c552bdf7105328c24a5..d334c963e16590c68ecd9d1d27434c7af6f7e21c 100644 +--- a/src/main/java/org/bukkit/Material.java ++++ b/src/main/java/org/bukkit/Material.java +@@ -11156,4 +11156,40 @@ public enum Material implements Keyed, Translatable, net.kyori.adventure.transla + public String getItemTranslationKey() { + return Bukkit.getUnsafe().getItemTranslationKey(this); + } ++ ++ // Purpur start ++ public boolean isArmor() { ++ switch (this) { ++ // ++ case LEATHER_BOOTS: ++ case LEATHER_CHESTPLATE: ++ case LEATHER_HELMET: ++ case LEATHER_LEGGINGS: ++ case CHAINMAIL_BOOTS: ++ case CHAINMAIL_CHESTPLATE: ++ case CHAINMAIL_HELMET: ++ case CHAINMAIL_LEGGINGS: ++ case IRON_BOOTS: ++ case IRON_CHESTPLATE: ++ case IRON_HELMET: ++ case IRON_LEGGINGS: ++ case GOLDEN_BOOTS: ++ case GOLDEN_CHESTPLATE: ++ case GOLDEN_HELMET: ++ case GOLDEN_LEGGINGS: ++ case DIAMOND_BOOTS: ++ case DIAMOND_CHESTPLATE: ++ case DIAMOND_HELMET: ++ case DIAMOND_LEGGINGS: ++ case NETHERITE_BOOTS: ++ case NETHERITE_CHESTPLATE: ++ case NETHERITE_HELMET: ++ case NETHERITE_LEGGINGS: ++ case TURTLE_HELMET: ++ return true; ++ default: ++ return false; ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/OfflinePlayer.java b/src/main/java/org/bukkit/OfflinePlayer.java +index 69b50eee42e8c52063033705bd23a5ef5231ed83..3578ab0c3a413d56bc39af43b5d3201d20d7d13a 100644 +--- a/src/main/java/org/bukkit/OfflinePlayer.java ++++ b/src/main/java/org/bukkit/OfflinePlayer.java +@@ -455,4 +455,114 @@ public interface OfflinePlayer extends ServerOperator, AnimalTamer, Configuratio + */ + @Nullable + public Location getLastDeathLocation(); ++ ++ // Purpur start - OfflinePlayer API ++ /** ++ * Determines if the OfflinePlayer is allowed to fly via jump key double-tap like ++ * in creative mode. ++ * ++ * @return True if the player is allowed to fly. ++ */ ++ public boolean getAllowFlight(); ++ ++ /** ++ * Sets if the OfflinePlayer is allowed to fly via jump key double-tap like in ++ * creative mode. ++ * ++ * @param flight If flight should be allowed. ++ */ ++ public void setAllowFlight(boolean flight); ++ ++ /** ++ * Checks to see if this player is currently flying or not. ++ * ++ * @return True if the player is flying, else false. ++ */ ++ public boolean isFlying(); ++ ++ /** ++ * Makes this player start or stop flying. ++ * ++ * @param value True to fly. ++ */ ++ public void setFlying(boolean value); ++ ++ /** ++ * Sets the speed at which a client will fly. Negative values indicate ++ * reverse directions. ++ * ++ * @param value The new speed, from -1 to 1. ++ * @throws IllegalArgumentException If new speed is less than -1 or ++ * greater than 1 ++ */ ++ public void setFlySpeed(float value) throws IllegalArgumentException; ++ ++ /** ++ * Sets the speed at which a client will walk. Negative values indicate ++ * reverse directions. ++ * ++ * @param value The new speed, from -1 to 1. ++ * @throws IllegalArgumentException If new speed is less than -1 or ++ * greater than 1 ++ */ ++ public void setWalkSpeed(float value) throws IllegalArgumentException; ++ ++ /** ++ * Gets the current allowed speed that a client can fly. ++ * ++ * @return The current allowed speed, from -1 to 1 ++ */ ++ public float getFlySpeed(); ++ ++ /** ++ * Gets the current allowed speed that a client can walk. ++ * ++ * @return The current allowed speed, from -1 to 1 ++ */ ++ public float getWalkSpeed(); ++ ++ /** ++ * Gets the entity's current position ++ * ++ * @return a new copy of Location containing the position of this offline player ++ */ ++ @Nullable ++ public Location getLocation(); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleport implementation. ++ * ++ * @param destination ++ * @return true if teleportation was successful ++ */ ++ public boolean teleportOffline(@NotNull org.bukkit.Location destination); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleport implementation. ++ * ++ * @param destination ++ * @param cause Teleport cause used if player is online ++ * @return true if teleportation was successful ++ */ ++ public boolean teleportOffline(@NotNull org.bukkit.Location destination, @NotNull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleportAsync implementation. ++ * ++ * @param destination ++ * @return true if teleportation successful ++ */ ++ @NotNull ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination); ++ ++ /** ++ * Sets OfflinePlayer's location. If player is online, it falls back to the Player#teleportAsync implementation. ++ * ++ * @param destination ++ * @param cause Teleport cause used if player is online ++ * @return true if teleportation successful ++ */ ++ @NotNull ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination, @NotNull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause); ++ // Purpur end - OfflinePlayer API + } +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index 6253e761c595c8b89d08f9d42fe3e19cadbf4918..bb4452abd1e6b169d03e77a6b6bdec813f300e1d 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -2021,6 +2021,18 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + + // Paper end + ++ // Purpur start ++ @NotNull ++ public org.bukkit.configuration.file.YamlConfiguration getPurpurConfig() { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ ++ @NotNull ++ public java.util.Properties getServerProperties() { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ // Purpur end ++ + /** + * Sends the component to the player + * +@@ -2215,4 +2227,111 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + long getLastTickOversleepTime(); + // Gale end - YAPFA - last tick time - API + ++ // Purpur start ++ /** ++ * Get the name of this server ++ * ++ * @return the name of the server ++ */ ++ @NotNull ++ String getServerName(); ++ ++ /** ++ * Check if server is lagging according to laggy threshold setting ++ * ++ * @return True if lagging ++ */ ++ boolean isLagging(); ++ ++ /** ++ * Add an Item as fuel for furnaces ++ * ++ * @param material The material that will be the fuel ++ * @param burnTime The time (in ticks) this item will burn for ++ */ ++ public void addFuel(@NotNull Material material, int burnTime); ++ ++ /** ++ * Remove an item as fuel for furnaces ++ * ++ * @param material The material that will no longer be a fuel ++ */ ++ public void removeFuel(@NotNull Material material); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on the server. ++ *

++ * Clients may be inconsistent in displaying it. ++ * ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Clears all debug block highlights for all players on the server. ++ */ ++ void clearBlockHighlights(); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java +index 0b8776355f724927ada826735d5e73f3fb6897d5..cf8b93aa5787e96f54e2ffe2a0f157b0e53e2c5c 100644 +--- a/src/main/java/org/bukkit/World.java ++++ b/src/main/java/org/bukkit/World.java +@@ -4018,6 +4018,86 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient + @Nullable + public DragonBattle getEnderDragonBattle(); + ++ // Purpur start ++ /** ++ * Gets the local difficulty (based on inhabited time) at a location ++ * ++ * @param location Location to check ++ * @return The local difficulty ++ */ ++ public float getLocalDifficultyAt(@NotNull Location location); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to all players on this world. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Clears all debug block highlights for all players on this world. ++ */ ++ void clearBlockHighlights(); ++ // Purpur end ++ + /** + * Represents various map environment types that a world may be + */ +diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java +index ac9a28922f8a556944a4c3649d74c32c622f0cb0..5349f16136d9348c374a7dfe5b89a71dfcb0e66d 100644 +--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java ++++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java +@@ -143,6 +143,19 @@ public class SimpleCommandMap implements CommandMap { + return false; + } + ++ // Purpur start ++ String[] parsedArgs = Arrays.copyOfRange(args, 1, args.length); ++ org.purpurmc.purpur.event.ExecuteCommandEvent event = new org.purpurmc.purpur.event.ExecuteCommandEvent(sender, target, sentCommandLabel, parsedArgs); ++ if (!event.callEvent()) { ++ return true; // cancelled ++ } ++ ++ sender = event.getSender(); ++ target = event.getCommand(); ++ sentCommandLabel = event.getLabel(); ++ parsedArgs = event.getArgs(); ++ // Purpur end ++ + // Paper start - Plugins do weird things to workaround normal registration + if (target.timings == null) { + target.timings = co.aikar.timings.TimingsManager.getCommandTiming(null, target); +@@ -150,10 +163,10 @@ public class SimpleCommandMap implements CommandMap { + // Paper end + + try { +- try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources ++ //try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources // Purpur + // Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false) +- target.execute(sender, sentCommandLabel, Arrays.copyOfRange(args, 1, args.length)); +- } // target.timings.stopTiming(); // Spigot // Paper ++ target.execute(sender, sentCommandLabel, parsedArgs); // Purpur ++ //} // target.timings.stopTiming(); // Spigot // Paper // Purpur + } catch (CommandException ex) { + server.getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper + //target.timings.stopTiming(); // Spigot // Paper +diff --git a/src/main/java/org/bukkit/command/defaults/VersionCommand.java b/src/main/java/org/bukkit/command/defaults/VersionCommand.java +index e40f017f87d6b6b4770501b106c76dc69ec69abb..eac5830986cd0638950bbb1e6f10a30e246e09a7 100644 +--- a/src/main/java/org/bukkit/command/defaults/VersionCommand.java ++++ b/src/main/java/org/bukkit/command/defaults/VersionCommand.java +@@ -198,7 +198,7 @@ public class VersionCommand extends BukkitCommand { + String version = Bukkit.getVersion(); + // Paper start + if (version.startsWith("null")) { // running from ide? +- setVersionMessage(net.kyori.adventure.text.Component.text("Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.YELLOW)); ++ setVersionMessage(net.kyori.adventure.text.Component.text("* Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.RED)); // Purpur + return; + } + setVersionMessage(getVersionFetcher().getVersionMessage(version)); +@@ -239,9 +239,11 @@ public class VersionCommand extends BukkitCommand { + // Paper start + private void setVersionMessage(final @NotNull net.kyori.adventure.text.Component msg) { + lastCheck = System.currentTimeMillis(); +- final net.kyori.adventure.text.Component message = net.kyori.adventure.text.TextComponent.ofChildren( +- net.kyori.adventure.text.Component.text(Bukkit.getVersionMessage(), net.kyori.adventure.text.format.NamedTextColor.WHITE), +- net.kyori.adventure.text.Component.newline(), ++ // Purpur start ++ int distance = getVersionFetcher().distance(); ++ final net.kyori.adventure.text.Component message = net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.separator(net.kyori.adventure.text.Component.newline()), ++ ChatColor.parseMM("Current: %s%s*", distance == 0 ? "" : distance > 0 ? "" : "", Bukkit.getVersion()), ++ // Purpur end + msg + ); + this.versionMessage = net.kyori.adventure.text.Component.text() +diff --git a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java +index bea786a8be4402f9384984e48390e745f2988dd6..5eb81fcc18b8fdec5a0e4c699525281fa6ad4d78 100644 +--- a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java ++++ b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java +@@ -228,6 +228,28 @@ public enum EnchantmentTarget { + public boolean includes(@NotNull Material item) { + return BREAKABLE.includes(item) || (WEARABLE.includes(item) && !item.equals(Material.ELYTRA)) || item.equals(Material.COMPASS); + } ++ // Purpur start ++ }, ++ ++ /** ++ * Allow the Enchantment to be placed on bows and crossbows. ++ */ ++ BOW_AND_CROSSBOW { ++ @Override ++ public boolean includes(@NotNull Material item) { ++ return item.equals(Material.BOW) || item.equals(Material.CROSSBOW); ++ } ++ }, ++ ++ /** ++ * Allow the Enchantment to be placed on shears. ++ */ ++ WEAPON_AND_SHEARS { ++ @Override ++ public boolean includes(@NotNull Material item) { ++ return WEAPON.includes(item) || item.equals(Material.SHEARS); ++ } ++ // Purpur end + }; + + /** +diff --git a/src/main/java/org/bukkit/entity/Endermite.java b/src/main/java/org/bukkit/entity/Endermite.java +index 138d2530de2410f4a9424dabd3e5ce0cd1c1dcd2..10a8d64ad2da0be2c14f34c3e7d1957c6f2883d1 100644 +--- a/src/main/java/org/bukkit/entity/Endermite.java ++++ b/src/main/java/org/bukkit/entity/Endermite.java +@@ -3,25 +3,21 @@ package org.bukkit.entity; + public interface Endermite extends Monster { + + /** +- * Gets whether this Endermite was spawned by a player. ++ * Gets whether this Endermite was spawned by a player from throwing an ender pearl + * +- * An Endermite spawned by a player will be attacked by nearby Enderman. ++ * An Endermite spawned by a player might be attacked by nearby Enderman depending on purpur.yml settings + * + * @return player spawned status +- * @deprecated this functionality no longer exists + */ +- @Deprecated + boolean isPlayerSpawned(); + + /** +- * Sets whether this Endermite was spawned by a player. ++ * Sets whether this Endermite was spawned by a player from throwing an ender pearl + * +- * An Endermite spawned by a player will be attacked by nearby Enderman. ++ * An Endermite spawned by a player might be attacked by nearby Enderman depending on purpur.yml settings + * + * @param playerSpawned player spawned status +- * @deprecated this functionality no longer exists + */ +- @Deprecated + void setPlayerSpawned(boolean playerSpawned); + // Paper start + /** +diff --git a/src/main/java/org/bukkit/entity/Entity.java b/src/main/java/org/bukkit/entity/Entity.java +index a2a423d4e4c2702ba5967223cab0432dd7d04732..cc78ce7de88a9a404ed20d5bc61b98d3107f29b3 100644 +--- a/src/main/java/org/bukkit/entity/Entity.java ++++ b/src/main/java/org/bukkit/entity/Entity.java +@@ -954,4 +954,55 @@ public interface Entity extends Metadatable, CommandSender, Nameable, Persistent + */ + boolean wouldCollideUsing(@NotNull BoundingBox boundingBox); + // Paper End - Collision API ++ ++ // Purpur start ++ /** ++ * Get the riding player ++ * ++ * @return Riding player ++ */ ++ @Nullable ++ Player getRider(); ++ ++ /** ++ * Check if entity is being ridden ++ * ++ * @return True if being ridden ++ */ ++ boolean hasRider(); ++ ++ /** ++ * Check if entity is ridable ++ * ++ * @return True if ridable ++ */ ++ boolean isRidable(); ++ ++ /** ++ * Check if entity is ridable in water ++ * ++ * @return True if ridable in water ++ */ ++ boolean isRidableInWater(); ++ ++ /** ++ * Checks if the entity is in daylight ++ * ++ * @return True if in daylight ++ */ ++ boolean isInDaylight(); ++ ++ /** ++ * Checks if the entity is fire immune ++ * ++ * @return True if fire immune ++ */ ++ boolean isImmuneToFire(); ++ ++ /** ++ * Sets if the entity is fire immune ++ * Set this to null to restore the entity type default ++ */ ++ void setImmuneToFire(@Nullable Boolean fireImmune); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/IronGolem.java b/src/main/java/org/bukkit/entity/IronGolem.java +index 655e37cb3a09610a3f3df805d6dcad17d722da62..09fd716c8fc9ea34a1cbf87bcbe22df035422a51 100644 +--- a/src/main/java/org/bukkit/entity/IronGolem.java ++++ b/src/main/java/org/bukkit/entity/IronGolem.java +@@ -19,4 +19,20 @@ public interface IronGolem extends Golem { + * player created, false if you want it to be a natural village golem. + */ + public void setPlayerCreated(boolean playerCreated); ++ ++ // Purpur start ++ /** ++ * Get the player that summoned this iron golem ++ * ++ * @return UUID of summoner ++ */ ++ @org.jetbrains.annotations.Nullable java.util.UUID getSummoner(); ++ ++ /** ++ * Set the player that summoned this iron golem ++ * ++ * @param summoner UUID of summoner ++ */ ++ void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Item.java b/src/main/java/org/bukkit/entity/Item.java +index 58017fce436cdbda255f7172fbdadb726d4b113c..05600fc8bf2a61aca8094029bc4c208a710da952 100644 +--- a/src/main/java/org/bukkit/entity/Item.java ++++ b/src/main/java/org/bukkit/entity/Item.java +@@ -153,4 +153,62 @@ public interface Item extends Entity, io.papermc.paper.entity.Frictional { // Pa + */ + public void setHealth(int health); + // Paper end ++ ++ // Purpur start ++ /** ++ * Set whether or not this item is immune to cactus ++ * ++ * @param immuneToCactus True to make immune to cactus ++ */ ++ void setImmuneToCactus(boolean immuneToCactus); ++ ++ /** ++ * Check if item is immune to cactus ++ * ++ * @return True if immune to cactus ++ */ ++ boolean isImmuneToCactus(); ++ ++ /** ++ * Set whether or not this item is immune to explosions ++ * ++ * @param immuneToExplosion True to make immune to explosions ++ */ ++ void setImmuneToExplosion(boolean immuneToExplosion); ++ ++ /** ++ * Check if item is immune to explosions ++ * ++ * @return True if immune to explosions ++ */ ++ boolean isImmuneToExplosion(); ++ ++ /** ++ * Set whether or not this item is immune to fire ++ * ++ * @param immuneToFire True to make immune to fire ++ */ ++ void setImmuneToFire(boolean immuneToFire); ++ ++ /** ++ * Check if item is immune to fire ++ * ++ * @return True if immune to fire ++ */ ++ boolean isImmuneToFire(); ++ ++ /** ++ * Set whether or not this item is immune to lightning ++ * ++ * @param immuneToLightning True to make immune to lightning ++ */ ++ void setImmuneToLightning(boolean immuneToLightning); ++ ++ /** ++ * Check if item is immune to lightning ++ * ++ * @return True if immune to lightning ++ */ ++ boolean isImmuneToLightning(); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/LivingEntity.java b/src/main/java/org/bukkit/entity/LivingEntity.java +index ffca32ae2464ea5a669029079a50585ca259a4f8..654dc0c6d98b29cf45d3826aece374726e3e9802 100644 +--- a/src/main/java/org/bukkit/entity/LivingEntity.java ++++ b/src/main/java/org/bukkit/entity/LivingEntity.java +@@ -1150,4 +1150,41 @@ public interface LivingEntity extends Attributable, Damageable, ProjectileSource + */ + void setBodyYaw(float bodyYaw); + // Paper end ++ ++ // Purpur start ++ /** ++ * Gets the distance (in blocks) this entity can safely fall without taking damage ++ * ++ * @return Safe fall distance ++ */ ++ float getSafeFallDistance(); ++ ++ /** ++ * Set the distance (in blocks) this entity can safely fall without taking damage ++ * ++ * @param safeFallDistance Safe fall distance ++ */ ++ void setSafeFallDistance(float safeFallDistance); ++ ++ /** ++ * Play item break animation for the item in specified equipment slot ++ * ++ * @param slot Equipment slot to play break animation for ++ */ ++ void broadcastItemBreak(@NotNull org.bukkit.inventory.EquipmentSlot slot); ++ ++ /** ++ * If this mob will burn in the sunlight ++ * ++ * @return True if mob will burn in sunlight ++ */ ++ boolean shouldBurnInDay(); ++ ++ /** ++ * Set if this mob should burn in the sunlight ++ * ++ * @param shouldBurnInDay True to burn in sunlight ++ */ ++ void setShouldBurnInDay(boolean shouldBurnInDay); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Llama.java b/src/main/java/org/bukkit/entity/Llama.java +index bc84b892cae5fe7019a3ad481e9da79956efa1fe..48eb5b00c460cccde29d327cef1d63fc04d6a829 100644 +--- a/src/main/java/org/bukkit/entity/Llama.java ++++ b/src/main/java/org/bukkit/entity/Llama.java +@@ -119,4 +119,20 @@ public interface Llama extends ChestedHorse, RangedEntity { // Paper + @org.jetbrains.annotations.Nullable + Llama getCaravanTail(); + // Paper end ++ ++ // Purpur start ++ /** ++ * Check if this Llama should attempt to join a caravan ++ * ++ * @return True if Llama is allowed to join a caravan ++ */ ++ boolean shouldJoinCaravan(); ++ ++ /** ++ * Set if this Llama should attempt to join a caravan ++ * ++ * @param shouldJoinCaravan True to allow joining a caravan ++ */ ++ void setShouldJoinCaravan(boolean shouldJoinCaravan); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java +index af82b2d96080c0657371d80a36192f937c5975bb..f5e2e1bdd31e74fdc00da5b503b178350e8235cd 100644 +--- a/src/main/java/org/bukkit/entity/Player.java ++++ b/src/main/java/org/bukkit/entity/Player.java +@@ -3047,4 +3047,139 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM + @Override + Spigot spigot(); + // Spigot end ++ ++ // Purpur start ++ /** ++ * Allows you to get if player uses Purpur Client ++ * ++ * @return True if Player uses Purpur Client ++ */ ++ public boolean usesPurpurClient(); ++ ++ /** ++ * Check if player is AFK ++ * ++ * @return True if AFK ++ */ ++ boolean isAfk(); ++ ++ /** ++ * Set player as AFK ++ * ++ * @param setAfk Whether to set AFK or not ++ */ ++ void setAfk(boolean setAfk); ++ ++ /** ++ * Reset the idle timer back to 0 ++ */ ++ void resetIdleTimer(); ++ ++ /** ++ * Check if player is invulnerable from recently spawning or accepting a resource pack ++ * ++ * @return True if invulnerable ++ */ ++ boolean isSpawnInvulnerable(); ++ ++ /** ++ * Get invulnerable ticks remaining ++ * ++ * @return Invulnerable ticks ++ */ ++ int getSpawnInvulnerableTicks(); ++ ++ /** ++ * Set invulnerable ticks remaining ++ * ++ * @param invulnerableTicks Invulnerable ticks remaining ++ */ ++ void setSpawnInvulnerableTicks(int invulnerableTicks); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param argb Color of the highlight. ARGB int. Will be ignored on some versions of vanilla client ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, int argb); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Creates debug block highlight on specified block location and show it to this player. ++ *

++ * Clients may be inconsistent in displaying it. ++ * @param location Location to highlight ++ * @param duration Duration for highlight to show in milliseconds ++ * @param text Text to show above the highlight ++ * @param color Color of the highlight. Will be ignored on some versions of vanilla client ++ * @param transparency Transparency of the highlight ++ * @throws IllegalArgumentException If transparency is outside 0-255 range ++ */ ++ void sendBlockHighlight(@NotNull Location location, int duration, @NotNull String text, @NotNull org.bukkit.Color color, int transparency); ++ ++ /** ++ * Clears all debug block highlights ++ */ ++ void clearBlockHighlights(); ++ ++ /** ++ * Sends a player the death screen with a specified death message. ++ * ++ * @param message The death message to show the player ++ */ ++ void sendDeathScreen(@NotNull net.kyori.adventure.text.Component message); ++ ++ /** ++ * Sends a player the death screen with a specified death message, ++ * along with the entity that caused the death. ++ * ++ * @param message The death message to show the player ++ * @param killer The entity that killed the player ++ */ ++ void sendDeathScreen(@NotNull net.kyori.adventure.text.Component message, @Nullable Entity killer); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Snowman.java b/src/main/java/org/bukkit/entity/Snowman.java +index 7fbfdb07585c7b28acea1f0c1f58ada0cc744441..21fcca092e2e31baa5ece0de9e44e3fade8c7123 100644 +--- a/src/main/java/org/bukkit/entity/Snowman.java ++++ b/src/main/java/org/bukkit/entity/Snowman.java +@@ -23,4 +23,20 @@ public interface Snowman extends Golem, RangedEntity, io.papermc.paper.entity.Sh + * @param derpMode True to remove the pumpkin, false to add a pumpkin + */ + void setDerp(boolean derpMode); ++ ++ // Purpur start ++ /** ++ * Get the player that summoned this snowman ++ * ++ * @return UUID of summoner ++ */ ++ @org.jetbrains.annotations.Nullable java.util.UUID getSummoner(); ++ ++ /** ++ * Set the player that summoned this snowman ++ * ++ * @param summoner UUID of summoner ++ */ ++ void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Villager.java b/src/main/java/org/bukkit/entity/Villager.java +index 3bc24457d143449e6a338d79becf7c39b9f81054..4a5edf4e72e81b22c1abb2ade244f7f4292e993c 100644 +--- a/src/main/java/org/bukkit/entity/Villager.java ++++ b/src/main/java/org/bukkit/entity/Villager.java +@@ -328,4 +328,14 @@ public interface Villager extends AbstractVillager { + */ + public void clearReputations(); + // Paper end ++ ++ // Purpur start ++ ++ /** ++ * Check if villager is currently lobotomized ++ * ++ * @return True if lobotomized ++ */ ++ boolean isLobotomized(); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Wither.java b/src/main/java/org/bukkit/entity/Wither.java +index a1b42ae35dda2da90ba00a2d6666514f7c5b11dd..3ccd61bf91d7746393589b0b35674361c2f1d133 100644 +--- a/src/main/java/org/bukkit/entity/Wither.java ++++ b/src/main/java/org/bukkit/entity/Wither.java +@@ -79,4 +79,20 @@ public interface Wither extends Monster, Boss, com.destroystokyo.paper.entity.Ra + */ + void setCanTravelThroughPortals(boolean value); + // Paper end ++ ++ // Purpur start ++ /** ++ * Get the player that summoned this wither ++ * ++ * @return UUID of summoner ++ */ ++ @org.jetbrains.annotations.Nullable java.util.UUID getSummoner(); ++ ++ /** ++ * Set the player that summoned this wither ++ * ++ * @param summoner UUID of summoner ++ */ ++ void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/entity/Wolf.java b/src/main/java/org/bukkit/entity/Wolf.java +index 84db38388bf7a58e66d6cd29620b4fe64b0a897e..82ebd99549ce9f9e6427a50cef424e9007735708 100644 +--- a/src/main/java/org/bukkit/entity/Wolf.java ++++ b/src/main/java/org/bukkit/entity/Wolf.java +@@ -69,4 +69,20 @@ public interface Wolf extends Tameable, Sittable, io.papermc.paper.entity.Collar + * @param interested Whether the wolf is interested + */ + public void setInterested(boolean interested); ++ ++ // Purpur start ++ /** ++ * Checks if this wolf is rabid ++ * ++ * @return whether the wolf is rabid ++ */ ++ public boolean isRabid(); ++ ++ /** ++ * Sets this wolf to be rabid or not ++ * ++ * @param rabid whether the wolf should be rabid ++ */ ++ public void setRabid(boolean rabid); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java +index c9f395064656dd0126410eb3c6e197baa450c063..13156a12e5df50cdc1e465dc0bd9d94108275629 100644 +--- a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java ++++ b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java +@@ -217,6 +217,12 @@ public class EntityPotionEffectEvent extends EntityEvent implements Cancellable + * When all effects are removed due to a bucket of milk. + */ + MILK, ++ // Purpur start ++ /** ++ * When a player wears full netherite armor ++ */ ++ NETHERITE_ARMOR, ++ // Purpur end + /** + * When a player gets bad omen after killing a patrol captain. + */ +diff --git a/src/main/java/org/bukkit/event/inventory/InventoryType.java b/src/main/java/org/bukkit/event/inventory/InventoryType.java +index a8e631315f2da68895a258cf0ba9875bc88fc48c..d5648ec745e3530aecf18c3e1f3185a5f63f3d11 100644 +--- a/src/main/java/org/bukkit/event/inventory/InventoryType.java ++++ b/src/main/java/org/bukkit/event/inventory/InventoryType.java +@@ -155,7 +155,7 @@ public enum InventoryType { + SMITHING_NEW(4, "Upgrade Gear"), + ; + +- private final int size; ++ private int size; public void setDefaultSize(int size) { this.size = size; } // Purpur - remove final and add setter + private final String title; + private final boolean isCreatable; + +diff --git a/src/main/java/org/bukkit/inventory/AnvilInventory.java b/src/main/java/org/bukkit/inventory/AnvilInventory.java +index c60be4fd24c7fdf65251dd6169e5e1ac3b588d95..569deccd2f1cf21da9b5906433ac493c1f2081be 100644 +--- a/src/main/java/org/bukkit/inventory/AnvilInventory.java ++++ b/src/main/java/org/bukkit/inventory/AnvilInventory.java +@@ -123,4 +123,14 @@ public interface AnvilInventory extends Inventory { + setItem(2, result); + } + // Paper end ++ ++ // Purpur start ++ boolean canBypassCost(); ++ ++ void setBypassCost(boolean bypassCost); ++ ++ boolean canDoUnsafeEnchants(); ++ ++ void setDoUnsafeEnchants(boolean canDoUnsafeEnchants); ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/inventory/ItemStack.java b/src/main/java/org/bukkit/inventory/ItemStack.java +index d15a74c38576c49df61cfab02c70fc5d8c0dd5f7..64055402076b62d32ba947830d935b79bae12d95 100644 +--- a/src/main/java/org/bukkit/inventory/ItemStack.java ++++ b/src/main/java/org/bukkit/inventory/ItemStack.java +@@ -17,6 +17,18 @@ import org.bukkit.inventory.meta.ItemMeta; + import org.bukkit.material.MaterialData; + import org.jetbrains.annotations.NotNull; + import org.jetbrains.annotations.Nullable; ++// Purpur start ++import com.google.common.collect.Multimap; ++import java.util.Collection; ++import org.bukkit.attribute.Attribute; ++import org.bukkit.attribute.AttributeModifier; ++import org.bukkit.block.data.BlockData; ++import org.bukkit.inventory.meta.BlockDataMeta; ++import org.bukkit.inventory.meta.Repairable; ++import org.bukkit.persistence.PersistentDataContainer; ++import org.bukkit.persistence.PersistentDataHolder; ++import com.destroystokyo.paper.Namespaced; ++// Purpur end + + /** + * Represents a stack of items. +@@ -986,4 +998,626 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat + return livingEntity.damageItemStack(this, amount); + } + // Paper end ++ ++ // Purpur start ++ /** ++ * Gets the display name that is set. ++ *

++ * Plugins should check that hasDisplayName() returns true ++ * before calling this method. ++ * ++ * @return the display name that is set ++ */ ++ @NotNull ++ public String getDisplayName() { ++ return getItemMeta().getDisplayName(); ++ } ++ ++ /** ++ * Sets the display name. ++ * ++ * @param name the name to set ++ */ ++ public void setDisplayName(@Nullable String name) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setDisplayName(name); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for existence of a display name. ++ * ++ * @return true if this has a display name ++ */ ++ public boolean hasDisplayName() { ++ return hasItemMeta() && getItemMeta().hasDisplayName(); ++ } ++ ++ /** ++ * Gets the localized display name that is set. ++ *

++ * Plugins should check that hasLocalizedName() returns true ++ * before calling this method. ++ * ++ * @return the localized name that is set ++ */ ++ @NotNull ++ public String getLocalizedName() { ++ return getItemMeta().getLocalizedName(); ++ } ++ ++ /** ++ * Sets the localized name. ++ * ++ * @param name the name to set ++ */ ++ public void setLocalizedName(@Nullable String name) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setLocalizedName(name); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for existence of a localized name. ++ * ++ * @return true if this has a localized name ++ */ ++ public boolean hasLocalizedName() { ++ return hasItemMeta() && getItemMeta().hasLocalizedName(); ++ } ++ ++ /** ++ * Checks for existence of lore. ++ * ++ * @return true if this has lore ++ */ ++ public boolean hasLore() { ++ return hasItemMeta() && getItemMeta().hasLore(); ++ } ++ ++ /** ++ * Checks for existence of the specified enchantment. ++ * ++ * @param ench enchantment to check ++ * @return true if this enchantment exists for this meta ++ */ ++ public boolean hasEnchant(@NotNull Enchantment ench) { ++ return hasItemMeta() && getItemMeta().hasEnchant(ench); ++ } ++ ++ /** ++ * Checks for the level of the specified enchantment. ++ * ++ * @param ench enchantment to check ++ * @return The level that the specified enchantment has, or 0 if none ++ */ ++ public int getEnchantLevel(@NotNull Enchantment ench) { ++ return getItemMeta().getEnchantLevel(ench); ++ } ++ ++ /** ++ * Returns a copy the enchantments in this ItemMeta.
++ * Returns an empty map if none. ++ * ++ * @return An immutable copy of the enchantments ++ */ ++ @NotNull ++ public Map getEnchants() { ++ return getItemMeta().getEnchants(); ++ } ++ ++ /** ++ * Adds the specified enchantment to this item meta. ++ * ++ * @param ench Enchantment to add ++ * @param level Level for the enchantment ++ * @param ignoreLevelRestriction this indicates the enchantment should be ++ * applied, ignoring the level limit ++ * @return true if the item meta changed as a result of this call, false ++ * otherwise ++ */ ++ public boolean addEnchant(@NotNull Enchantment ench, int level, boolean ignoreLevelRestriction) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.addEnchant(ench, level, ignoreLevelRestriction); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Removes the specified enchantment from this item meta. ++ * ++ * @param ench Enchantment to remove ++ * @return true if the item meta changed as a result of this call, false ++ * otherwise ++ */ ++ public boolean removeEnchant(@NotNull Enchantment ench) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeEnchant(ench); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Checks for the existence of any enchantments. ++ * ++ * @return true if an enchantment exists on this meta ++ */ ++ public boolean hasEnchants() { ++ return hasItemMeta() && getItemMeta().hasEnchants(); ++ } ++ ++ /** ++ * Checks if the specified enchantment conflicts with any enchantments in ++ * this ItemMeta. ++ * ++ * @param ench enchantment to test ++ * @return true if the enchantment conflicts, false otherwise ++ */ ++ public boolean hasConflictingEnchant(@NotNull Enchantment ench) { ++ return hasItemMeta() && getItemMeta().hasConflictingEnchant(ench); ++ } ++ ++ /** ++ * Sets the custom model data. ++ *

++ * CustomModelData is an integer that may be associated client side with a ++ * custom item model. ++ * ++ * @param data the data to set, or null to clear ++ */ ++ public void setCustomModelData(@Nullable Integer data) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setCustomModelData(data); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the custom model data that is set. ++ *

++ * CustomModelData is an integer that may be associated client side with a ++ * custom item model. ++ *

++ * Plugins should check that hasCustomModelData() returns true ++ * before calling this method. ++ * ++ * @return the localized name that is set ++ */ ++ public int getCustomModelData() { ++ return getItemMeta().getCustomModelData(); ++ } ++ ++ /** ++ * Checks for existence of custom model data. ++ *

++ * CustomModelData is an integer that may be associated client side with a ++ * custom item model. ++ * ++ * @return true if this has custom model data ++ */ ++ public boolean hasCustomModelData() { ++ return hasItemMeta() && getItemMeta().hasCustomModelData(); ++ } ++ ++ /** ++ * Returns whether the item has block data currently attached to it. ++ * ++ * @return whether block data is already attached ++ */ ++ public boolean hasBlockData() { ++ return hasItemMeta() && ((BlockDataMeta) getItemMeta()).hasBlockData(); ++ } ++ ++ /** ++ * Returns the currently attached block data for this item or creates a new ++ * one if one doesn't exist. ++ * ++ * The state is a copy, it must be set back (or to another item) with ++ * {@link #setBlockData(BlockData)} ++ * ++ * @param material the material we wish to get this data in the context of ++ * @return the attached data or new data ++ */ ++ @NotNull ++ public BlockData getBlockData(@NotNull Material material) { ++ return ((BlockDataMeta) getItemMeta()).getBlockData(material); ++ } ++ ++ /** ++ * Attaches a copy of the passed block data to the item. ++ * ++ * @param blockData the block data to attach to the block. ++ * @throws IllegalArgumentException if the blockData is null or invalid for ++ * this item. ++ */ ++ public void setBlockData(@NotNull BlockData blockData) { ++ ItemMeta itemMeta = getItemMeta(); ++ ((BlockDataMeta) itemMeta).setBlockData(blockData); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the repair penalty ++ * ++ * @return the repair penalty ++ */ ++ public int getRepairCost() { ++ return ((Repairable) getItemMeta()).getRepairCost(); ++ } ++ ++ /** ++ * Sets the repair penalty ++ * ++ * @param cost repair penalty ++ */ ++ public void setRepairCost(int cost) { ++ ItemMeta itemMeta = getItemMeta(); ++ ((Repairable) itemMeta).setRepairCost(cost); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks to see if this has a repair penalty ++ * ++ * @return true if this has a repair penalty ++ */ ++ public boolean hasRepairCost() { ++ return hasItemMeta() && ((Repairable) getItemMeta()).hasRepairCost(); ++ } ++ ++ /** ++ * Return if the unbreakable tag is true. An unbreakable item will not lose ++ * durability. ++ * ++ * @return true if the unbreakable tag is true ++ */ ++ public boolean isUnbreakable() { ++ return hasItemMeta() && getItemMeta().isUnbreakable(); ++ } ++ ++ /** ++ * Sets the unbreakable tag. An unbreakable item will not lose durability. ++ * ++ * @param unbreakable true if set unbreakable ++ */ ++ public void setUnbreakable(boolean unbreakable) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setUnbreakable(unbreakable); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for the existence of any AttributeModifiers. ++ * ++ * @return true if any AttributeModifiers exist ++ */ ++ public boolean hasAttributeModifiers() { ++ return hasItemMeta() && getItemMeta().hasAttributeModifiers(); ++ } ++ ++ /** ++ * Return an immutable copy of all Attributes and ++ * their modifiers in this ItemMeta.
++ * Returns null if none exist. ++ * ++ * @return an immutable {@link Multimap} of Attributes ++ * and their AttributeModifiers, or null if none exist ++ */ ++ @Nullable ++ public Multimap getAttributeModifiers() { ++ return getItemMeta().getAttributeModifiers(); ++ } ++ ++ /** ++ * Return an immutable copy of all {@link Attribute}s and their ++ * {@link AttributeModifier}s for a given {@link EquipmentSlot}.
++ * Any {@link AttributeModifier} that does have have a given ++ * {@link EquipmentSlot} will be returned. This is because ++ * AttributeModifiers without a slot are active in any slot.
++ * If there are no attributes set for the given slot, an empty map ++ * will be returned. ++ * ++ * @param slot the {@link EquipmentSlot} to check ++ * @return the immutable {@link Multimap} with the ++ * respective Attributes and modifiers, or an empty map ++ * if no attributes are set. ++ */ ++ @NotNull ++ public Multimap getAttributeModifiers(@Nullable EquipmentSlot slot) { ++ return getItemMeta().getAttributeModifiers(slot); ++ } ++ ++ /** ++ * Return an immutable copy of all {@link AttributeModifier}s ++ * for a given {@link Attribute} ++ * ++ * @param attribute the {@link Attribute} ++ * @return an immutable collection of {@link AttributeModifier}s ++ * or null if no AttributeModifiers exist for the Attribute. ++ * @throws NullPointerException if Attribute is null ++ */ ++ @Nullable ++ public Collection getAttributeModifiers(@NotNull Attribute attribute) { ++ return getItemMeta().getAttributeModifiers(attribute); ++ } ++ ++ /** ++ * Add an Attribute and it's Modifier. ++ * AttributeModifiers can now support {@link EquipmentSlot}s. ++ * If not set, the {@link AttributeModifier} will be active in ALL slots. ++ *
++ * Two {@link AttributeModifier}s that have the same {@link java.util.UUID} ++ * cannot exist on the same Attribute. ++ * ++ * @param attribute the {@link Attribute} to modify ++ * @param modifier the {@link AttributeModifier} specifying the modification ++ * @return true if the Attribute and AttributeModifier were ++ * successfully added ++ * @throws NullPointerException if Attribute is null ++ * @throws NullPointerException if AttributeModifier is null ++ * @throws IllegalArgumentException if AttributeModifier already exists ++ */ ++ public boolean addAttributeModifier(@NotNull Attribute attribute, @NotNull AttributeModifier modifier) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.addAttributeModifier(attribute, modifier); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Set all {@link Attribute}s and their {@link AttributeModifier}s. ++ * To clear all currently set Attributes and AttributeModifiers use ++ * null or an empty Multimap. ++ * If not null nor empty, this will filter all entries that are not-null ++ * and add them to the ItemStack. ++ * ++ * @param attributeModifiers the new Multimap containing the Attributes ++ * and their AttributeModifiers ++ */ ++ public void setAttributeModifiers(@Nullable Multimap attributeModifiers) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setAttributeModifiers(attributeModifiers); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Remove all {@link AttributeModifier}s associated with the given ++ * {@link Attribute}. ++ * This will return false if nothing was removed. ++ * ++ * @param attribute attribute to remove ++ * @return true if all modifiers were removed from a given ++ * Attribute. Returns false if no attributes were ++ * removed. ++ * @throws NullPointerException if Attribute is null ++ */ ++ public boolean removeAttributeModifier(@NotNull Attribute attribute) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeAttributeModifier(attribute); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Remove all {@link Attribute}s and {@link AttributeModifier}s for a ++ * given {@link EquipmentSlot}.
++ * If the given {@link EquipmentSlot} is null, this will remove all ++ * {@link AttributeModifier}s that do not have an EquipmentSlot set. ++ * ++ * @param slot the {@link EquipmentSlot} to clear all Attributes and ++ * their modifiers for ++ * @return true if all modifiers were removed that match the given ++ * EquipmentSlot. ++ */ ++ public boolean removeAttributeModifier(@Nullable EquipmentSlot slot) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeAttributeModifier(slot); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Remove a specific {@link Attribute} and {@link AttributeModifier}. ++ * AttributeModifiers are matched according to their {@link java.util.UUID}. ++ * ++ * @param attribute the {@link Attribute} to remove ++ * @param modifier the {@link AttributeModifier} to remove ++ * @return if any attribute modifiers were remove ++ * ++ * @throws NullPointerException if the Attribute is null ++ * @throws NullPointerException if the AttributeModifier is null ++ * ++ * @see AttributeModifier#getUniqueId() ++ */ ++ public boolean removeAttributeModifier(@NotNull Attribute attribute, @NotNull AttributeModifier modifier) { ++ ItemMeta itemMeta = getItemMeta(); ++ boolean result = itemMeta.removeAttributeModifier(attribute, modifier); ++ setItemMeta(itemMeta); ++ return result; ++ } ++ ++ /** ++ * Returns a custom tag container capable of storing tags on the object. ++ * ++ * Note that the tags stored on this container are all stored under their ++ * own custom namespace therefore modifying default tags using this ++ * {@link PersistentDataHolder} is impossible. ++ * ++ * @return the persistent metadata container ++ */ ++ @NotNull ++ public PersistentDataContainer getPersistentDataContainer() { ++ return getItemMeta().getPersistentDataContainer(); ++ } ++ ++ /** ++ * Checks to see if this item has damage ++ * ++ * @return true if this has damage ++ */ ++ public boolean hasDamage() { ++ return hasItemMeta() && ((Damageable) getItemMeta()).hasDamage(); ++ } ++ ++ /** ++ * Gets the damage ++ * ++ * @return the damage ++ */ ++ public int getDamage() { ++ return ((Damageable) getItemMeta()).getDamage(); ++ } ++ ++ /** ++ * Sets the damage ++ * ++ * @param damage item damage ++ */ ++ public void setDamage(int damage) { ++ ItemMeta itemMeta = getItemMeta(); ++ ((Damageable) itemMeta).setDamage(damage); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the collection of namespaced keys that the item can destroy in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @return Set of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ @NotNull ++ public java.util.Set getDestroyableKeys() { ++ return getItemMeta().getDestroyableKeys(); ++ } ++ ++ /** ++ * Sets the collection of namespaced keys that the item can destroy in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @param canDestroy Collection of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ public void setDestroyableKeys(@NotNull Collection canDestroy) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setDestroyableKeys(canDestroy); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Gets the collection of namespaced keys that the item can be placed on in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @return Set of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ @NotNull ++ public java.util.Set getPlaceableKeys() { ++ return getItemMeta().getPlaceableKeys(); ++ } ++ ++ /** ++ * Sets the set of namespaced keys that the item can be placed on in {@link org.bukkit.GameMode#ADVENTURE} ++ * ++ * @param canPlaceOn Collection of {@link com.destroystokyo.paper.Namespaced} ++ */ ++ @NotNull ++ public void setPlaceableKeys(@NotNull Collection canPlaceOn) { ++ ItemMeta itemMeta = getItemMeta(); ++ itemMeta.setPlaceableKeys(canPlaceOn); ++ setItemMeta(itemMeta); ++ } ++ ++ /** ++ * Checks for the existence of any keys that the item can be placed on ++ * ++ * @return true if this item has placeable keys ++ */ ++ public boolean hasPlaceableKeys() { ++ return hasItemMeta() && getItemMeta().hasPlaceableKeys(); ++ } ++ ++ /** ++ * Checks for the existence of any keys that the item can destroy ++ * ++ * @return true if this item has destroyable keys ++ */ ++ public boolean hasDestroyableKeys() { ++ return hasItemMeta() && getItemMeta().hasDestroyableKeys(); ++ } ++ ++ /** ++ * Repairs this item by 1 durability ++ */ ++ public void repair() { ++ repair(1); ++ } ++ ++ /** ++ * Damages this item by 1 durability ++ * ++ * @return True if damage broke the item ++ */ ++ public boolean damage() { ++ return damage(1); ++ } ++ ++ /** ++ * Repairs this item's durability by amount ++ * ++ * @param amount Amount of durability to repair ++ */ ++ public void repair(int amount) { ++ damage(-amount); ++ } ++ ++ /** ++ * Damages this item's durability by amount ++ * ++ * @param amount Amount of durability to damage ++ * @return True if damage broke the item ++ */ ++ public boolean damage(int amount) { ++ return damage(amount, false); ++ } ++ ++ /** ++ * Damages this item's durability by amount ++ * ++ * @param amount Amount of durability to damage ++ * @param ignoreUnbreaking Ignores unbreaking enchantment ++ * @return True if damage broke the item ++ */ ++ public boolean damage(int amount, boolean ignoreUnbreaking) { ++ Damageable damageable = (Damageable) getItemMeta(); ++ if (amount > 0) { ++ int unbreaking = getEnchantLevel(Enchantment.DURABILITY); ++ int reduce = 0; ++ for (int i = 0; unbreaking > 0 && i < amount; ++i) { ++ if (reduceDamage(java.util.concurrent.ThreadLocalRandom.current(), unbreaking)) { ++ ++reduce; ++ } ++ } ++ amount -= reduce; ++ if (amount <= 0) { ++ return isBroke(damageable.getDamage()); ++ } ++ } ++ int damage = damageable.getDamage() + amount; ++ damageable.setDamage(damage); ++ setItemMeta((ItemMeta) damageable); ++ return isBroke(damage); ++ } ++ ++ public boolean isBroke(int damage) { ++ if (damage > getType().getMaxDurability()) { ++ if (getAmount() > 0) { ++ // ensure it "breaks" ++ setAmount(0); ++ } ++ return true; ++ } ++ return false; ++ } ++ ++ private boolean reduceDamage(java.util.Random random, int unbreaking) { ++ if (getType().isArmor()) { ++ return random.nextFloat() < 0.6F; ++ } ++ return random.nextInt(unbreaking + 1) > 0; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/inventory/RecipeChoice.java b/src/main/java/org/bukkit/inventory/RecipeChoice.java +index 90208bc96085f05a3b657b9467b1670d00b03104..c59d5e4ef9641fd73463b177239226866272745b 100644 +--- a/src/main/java/org/bukkit/inventory/RecipeChoice.java ++++ b/src/main/java/org/bukkit/inventory/RecipeChoice.java +@@ -10,6 +10,7 @@ import java.util.function.Predicate; + import org.bukkit.Material; + import org.bukkit.Tag; + import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; // Purpur + + /** + * Represents a potential item match within a recipe. All choices within a +@@ -152,6 +153,7 @@ public interface RecipeChoice extends Predicate, Cloneable { + public static class ExactChoice implements RecipeChoice { + + private List choices; ++ private Predicate predicate; // Purpur + + public ExactChoice(@NotNull ItemStack stack) { + this(Arrays.asList(stack)); +@@ -196,6 +198,7 @@ public interface RecipeChoice extends Predicate, Cloneable { + + @Override + public boolean test(@NotNull ItemStack t) { ++ if (predicate != null) return predicate.test(t); // Purpur + for (ItemStack match : choices) { + if (t.isSimilar(match)) { + return true; +@@ -205,6 +208,17 @@ public interface RecipeChoice extends Predicate, Cloneable { + return false; + } + ++ // Purpur start ++ @Nullable ++ public Predicate getPredicate() { ++ return predicate; ++ } ++ ++ public void setPredicate(@Nullable Predicate predicate) { ++ this.predicate = predicate; ++ } ++ // Purpur end ++ + @Override + public int hashCode() { + int hash = 7; +diff --git a/src/main/java/org/bukkit/permissions/PermissibleBase.java b/src/main/java/org/bukkit/permissions/PermissibleBase.java +index cd3296fea01648592d2af89b3d80135acb6d0958..45797a6fbae1d8edc4211cb30def24ad4f59bd49 100644 +--- a/src/main/java/org/bukkit/permissions/PermissibleBase.java ++++ b/src/main/java/org/bukkit/permissions/PermissibleBase.java +@@ -168,7 +168,7 @@ public class PermissibleBase implements Permissible { + + for (Permission perm : defaults) { + String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH); +- permissions.put(name, new PermissionAttachmentInfo(parent, name, null, true)); ++ permissions.put(name, new PermissionAttachmentInfo(parent, name, null, perm.getDefault().getValue(isOp()))); // Purpur + Bukkit.getServer().getPluginManager().subscribeToPermission(name, parent); + calculateChildPermissions(perm.getChildren(), false, null); + } +@@ -196,7 +196,7 @@ public class PermissibleBase implements Permissible { + String name = entry.getKey(); + + Permission perm = Bukkit.getServer().getPluginManager().getPermission(name); +- boolean value = entry.getValue() ^ invert; ++ boolean value = (entry.getValue() == null && perm != null ? perm.getDefault().getValue(isOp()) : entry.getValue()) ^ invert; // Purpur + String lname = name.toLowerCase(java.util.Locale.ENGLISH); + + permissions.put(lname, new PermissionAttachmentInfo(parent, lname, attachment, value)); +diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +index 301e82369603f3dd6e6c1bd380da4bacacd7ef6c..0c6ca7588fb3d6b6497ddf032fe75e5c6c9719e5 100644 +--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +@@ -55,6 +55,7 @@ public final class JavaPluginLoader implements PluginLoader { + private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")}; + private final List loaders = new CopyOnWriteArrayList(); + private final LibraryLoader libraryLoader; ++ public static boolean SuppressLibraryLoaderLogger = false; // Purpur + + /** + * This class was not meant to be constructed explicitly +diff --git a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java +index e4b6f278a811acbb0070e311c5c3bdaff7b00474..ee83ecb054099cb85168a9499dfe967a0a9ec796 100644 +--- a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java +@@ -65,6 +65,7 @@ public class LibraryLoader + @Override + public void transferStarted(@NotNull TransferEvent event) throws TransferCancelledException + { ++ if (!JavaPluginLoader.SuppressLibraryLoaderLogger) // Purpur + logger.log( Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName() ); + } + } ); +@@ -80,6 +81,7 @@ public class LibraryLoader + { + return null; + } ++ if (!JavaPluginLoader.SuppressLibraryLoaderLogger) // Purpur + logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[] + { + java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), desc.getLibraries().size() // Paper - use configured log prefix +@@ -118,6 +120,7 @@ public class LibraryLoader + } + + jarFiles.add( url ); ++ if (!JavaPluginLoader.SuppressLibraryLoaderLogger) // Purpur + logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[] + { + java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), file // Paper - use configured log prefix +diff --git a/src/main/java/org/bukkit/potion/PotionEffect.java b/src/main/java/org/bukkit/potion/PotionEffect.java +index ccdca0d75868135dc7b96daeff2236b225c4add1..cad9f4ddc6be23c595e79419872f8f026703cb80 100644 +--- a/src/main/java/org/bukkit/potion/PotionEffect.java ++++ b/src/main/java/org/bukkit/potion/PotionEffect.java +@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap; + import java.util.Map; + import java.util.NoSuchElementException; + import org.bukkit.Color; ++import org.bukkit.NamespacedKey; + import org.bukkit.configuration.serialization.ConfigurationSerializable; + import org.bukkit.configuration.serialization.SerializableAs; + import org.bukkit.entity.LivingEntity; +@@ -31,12 +32,14 @@ public class PotionEffect implements ConfigurationSerializable { + private static final String AMBIENT = "ambient"; + private static final String PARTICLES = "has-particles"; + private static final String ICON = "has-icon"; ++ private static final String KEY = "namespacedKey"; // Purpur + private final int amplifier; + private final int duration; + private final PotionEffectType type; + private final boolean ambient; + private final boolean particles; + private final boolean icon; ++ @Nullable private final NamespacedKey key; // Purpur + + /** + * Creates a potion effect. +@@ -49,6 +52,36 @@ public class PotionEffect implements ConfigurationSerializable { + * @param icon the icon status, see {@link PotionEffect#hasIcon()} + */ + public PotionEffect(@NotNull PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, boolean icon) { ++ // Purpur start ++ this(type, duration, amplifier, ambient, particles, icon, null); ++ } ++ ++ /** ++ * Create a potion effect. ++ * @param duration measured in ticks, see {@link ++ * PotionEffect#getDuration()} ++ * @param amplifier the amplifier, see {@link PotionEffect#getAmplifier()} ++ * @param ambient the ambient status, see {@link PotionEffect#isAmbient()} ++ * @param particles the particle status, see {@link PotionEffect#hasParticles()} ++ * @param key the namespacedKey, see {@link PotionEffect#getKey()} ++ */ ++ public PotionEffect(@NotNull PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, @Nullable NamespacedKey key) { ++ this(type, duration, amplifier, ambient, particles, particles, key); ++ } ++ ++ /** ++ * Creates a potion effect. ++ * @param type effect type ++ * @param duration measured in ticks, see {@link ++ * PotionEffect#getDuration()} ++ * @param amplifier the amplifier, see {@link PotionEffect#getAmplifier()} ++ * @param ambient the ambient status, see {@link PotionEffect#isAmbient()} ++ * @param particles the particle status, see {@link PotionEffect#hasParticles()} ++ * @param icon the icon status, see {@link PotionEffect#hasIcon()} ++ * @param key the namespacedKey, see {@link PotionEffect#getKey()} ++ */ ++ public PotionEffect(@NotNull PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, boolean icon, @Nullable NamespacedKey key) { ++ // Purpur end + Preconditions.checkArgument(type != null, "effect type cannot be null"); + this.type = type; + this.duration = duration; +@@ -56,6 +89,7 @@ public class PotionEffect implements ConfigurationSerializable { + this.ambient = ambient; + this.particles = particles; + this.icon = icon; ++ this.key = key; // Purpur - add key + } + + /** +@@ -103,36 +137,43 @@ public class PotionEffect implements ConfigurationSerializable { + * @param map the map to deserialize from + */ + public PotionEffect(@NotNull Map map) { +- this(getEffectType(map), getInt(map, DURATION), getInt(map, AMPLIFIER), getBool(map, AMBIENT, false), getBool(map, PARTICLES, true), getBool(map, ICON, getBool(map, PARTICLES, true))); ++ this(getEffectType(map), getInt(map, DURATION), getInt(map, AMPLIFIER), getBool(map, AMBIENT, false), getBool(map, PARTICLES, true), getBool(map, ICON, getBool(map, PARTICLES, true)), getKey(map)); // Purpur - getKey + } + + // Paper start + @NotNull + public PotionEffect withType(@NotNull PotionEffectType type) { +- return new PotionEffect(type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withDuration(int duration) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withAmplifier(int amplifier) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withAmbient(boolean ambient) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withParticles(boolean particles) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + @NotNull + public PotionEffect withIcon(boolean icon) { +- return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon); ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); // Purpur - add key + } + // Paper end + ++ // Purpur start ++ @NotNull ++ public PotionEffect withKey(@Nullable NamespacedKey key) { ++ return new PotionEffect(this.type, duration, amplifier, ambient, particles, icon, key); ++ } ++ // Purpur end ++ + @NotNull + private static PotionEffectType getEffectType(@NotNull Map map) { + int type = getInt(map, TYPE); +@@ -159,17 +200,33 @@ public class PotionEffect implements ConfigurationSerializable { + return def; + } + ++ // Purpur start ++ @Nullable ++ private static NamespacedKey getKey(@NotNull Map map) { ++ Object key = map.get(KEY); ++ if (key instanceof String stringKey) { ++ return NamespacedKey.fromString(stringKey); ++ } ++ return null; ++ } ++ // Purpur end ++ + @Override + @NotNull + public Map serialize() { +- return ImmutableMap.builder() +- .put(TYPE, type.getId()) +- .put(DURATION, duration) +- .put(AMPLIFIER, amplifier) +- .put(AMBIENT, ambient) +- .put(PARTICLES, particles) +- .put(ICON, icon) +- .build(); ++ // Purpur start - add key, don't serialize if null. ++ ImmutableMap.Builder builder = ImmutableMap.builder() ++ .put(TYPE, type.getId()) ++ .put(DURATION, duration) ++ .put(AMPLIFIER, amplifier) ++ .put(AMBIENT, ambient) ++ .put(PARTICLES, particles) ++ .put(ICON, icon); ++ if(key != null) { ++ builder.put(KEY, key.toString()); ++ } ++ return builder.build(); ++ // Purpur end + } + + /** +@@ -193,7 +250,7 @@ public class PotionEffect implements ConfigurationSerializable { + return false; + } + PotionEffect that = (PotionEffect) obj; +- return this.type.equals(that.type) && this.ambient == that.ambient && this.amplifier == that.amplifier && this.duration == that.duration && this.particles == that.particles && this.icon == that.icon; ++ return this.type.equals(that.type) && this.ambient == that.ambient && this.amplifier == that.amplifier && this.duration == that.duration && this.particles == that.particles && this.icon == that.icon && this.key == that.key; // Purpur - add key + } + + /** +@@ -289,6 +346,24 @@ public class PotionEffect implements ConfigurationSerializable { + return icon; + } + ++ ++ // Purpur start ++ /** ++ * @return if the key isn't the default namespacedKey ++ */ ++ public boolean hasKey() { ++ return key != null; ++ } ++ ++ /** ++ * @return the key attached to the potion ++ */ ++ @Nullable ++ public NamespacedKey getKey() { ++ return key; ++ } ++ // Purpur end ++ + @Override + public int hashCode() { + int hash = 1; +@@ -303,6 +378,6 @@ public class PotionEffect implements ConfigurationSerializable { + + @Override + public String toString() { +- return type.getName() + (ambient ? ":(" : ":") + duration + "t-x" + amplifier + (ambient ? ")" : ""); ++ return type.getName() + (ambient ? ":(" : ":") + duration + "t-x" + amplifier + (ambient ? ")" : "") + (hasKey() ? "(" + key + ")" : ""); // Purpur - add key if not null + } + } +diff --git a/src/main/java/org/bukkit/util/permissions/CommandPermissions.java b/src/main/java/org/bukkit/util/permissions/CommandPermissions.java +index 7763d6101ac61900db1e2310966b99584539fd0e..d5a42707d365ffd72532bbb1a59a1ca7145f9918 100644 +--- a/src/main/java/org/bukkit/util/permissions/CommandPermissions.java ++++ b/src/main/java/org/bukkit/util/permissions/CommandPermissions.java +@@ -18,6 +18,7 @@ public final class CommandPermissions { + DefaultPermissions.registerPermission(PREFIX + "plugins", "Allows the user to view the list of plugins running on this server", PermissionDefault.TRUE, commands); + DefaultPermissions.registerPermission(PREFIX + "reload", "Allows the user to reload the server settings", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(PREFIX + "version", "Allows the user to view the version of the server", PermissionDefault.TRUE, commands); ++ DefaultPermissions.registerPermission(PREFIX + "purpur", "Allows the user to use the purpur command", PermissionDefault.OP, commands); // Purpur + + commands.recalculatePermissibles(); + return commands; +diff --git a/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java b/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java +index e1a4ddf2c07cdd242fa8054a0152522fe4039e85..10627d2a11251a8cb01bbc3f6242d66f3505a16e 100644 +--- a/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java ++++ b/src/main/java/org/bukkit/util/permissions/DefaultPermissions.java +@@ -31,7 +31,7 @@ public final class DefaultPermissions { + + if (withLegacy) { + Permission legacy = new Permission(LEGACY_PREFIX + result.getName(), result.getDescription(), PermissionDefault.FALSE); +- legacy.getChildren().put(result.getName(), true); ++ legacy.getChildren().put(result.getName(), null); // Purpur + registerPermission(perm, false); + } + +@@ -40,7 +40,7 @@ public final class DefaultPermissions { + + @NotNull + public static Permission registerPermission(@NotNull Permission perm, @NotNull Permission parent) { +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return registerPermission(perm); + } + +@@ -53,7 +53,7 @@ public final class DefaultPermissions { + @NotNull + public static Permission registerPermission(@NotNull String name, @Nullable String desc, @NotNull Permission parent) { + Permission perm = registerPermission(name, desc); +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return perm; + } + +@@ -66,7 +66,7 @@ public final class DefaultPermissions { + @NotNull + public static Permission registerPermission(@NotNull String name, @Nullable String desc, @Nullable PermissionDefault def, @NotNull Permission parent) { + Permission perm = registerPermission(name, desc, def); +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return perm; + } + +@@ -79,7 +79,7 @@ public final class DefaultPermissions { + @NotNull + public static Permission registerPermission(@NotNull String name, @Nullable String desc, @Nullable PermissionDefault def, @Nullable Map children, @NotNull Permission parent) { + Permission perm = registerPermission(name, desc, def, children); +- parent.getChildren().put(perm.getName(), true); ++ parent.getChildren().put(perm.getName(), null); // Purpur + return perm; + } + +@@ -89,6 +89,8 @@ public final class DefaultPermissions { + CommandPermissions.registerPermissions(parent); + BroadcastPermissions.registerPermissions(parent); + ++ PurpurPermissions.registerPermissions(); // Purpur ++ + parent.recalculatePermissibles(); + } + } +diff --git a/src/main/java/org/bukkit/util/permissions/PurpurPermissions.java b/src/main/java/org/bukkit/util/permissions/PurpurPermissions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..baec4c87d7ea4d54934ca22fd1eb7b46dd69061b +--- /dev/null ++++ b/src/main/java/org/bukkit/util/permissions/PurpurPermissions.java +@@ -0,0 +1,87 @@ ++package org.bukkit.util.permissions; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.EntityType; ++import org.bukkit.entity.Mob; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class PurpurPermissions { ++ private static final String ROOT = "purpur"; ++ private static final String PREFIX = ROOT + "."; ++ private static final Set mobs = new HashSet<>(); ++ ++ static { ++ for (EntityType mob : EntityType.values()) { ++ Class clazz = mob.getEntityClass(); ++ if (clazz != null && Mob.class.isAssignableFrom(clazz)) { ++ mobs.add(mob.getName()); ++ } ++ } ++ } ++ ++ @NotNull ++ public static Permission registerPermissions() { ++ Permission purpur = DefaultPermissions.registerPermission(ROOT, "Gives the user the ability to use all Purpur utilities and commands", PermissionDefault.FALSE); ++ ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.six", "Gives the user six rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.five", "Gives the user five rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.four", "Gives the user four rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.three", "Gives the user three rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.two", "Gives the user two rows of enderchest space", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "enderchest.rows.one", "Gives the user one row of enderchest space", PermissionDefault.FALSE, purpur); ++ ++ DefaultPermissions.registerPermission(PREFIX + "debug.f3n", "Allows the user to use F3+N keybind to swap gamemodes", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "joinfullserver", "Allows the user to join a full server", PermissionDefault.OP, purpur); ++ ++ DefaultPermissions.registerPermission(PREFIX + "drop.spawners", "Allows the user to drop spawner cage when broken with diamond pickaxe with silk touch", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "place.spawners", "Allows the user to place spawner cage in the world", PermissionDefault.FALSE, purpur); ++ ++ DefaultPermissions.registerPermission(PREFIX + "mending_shift_click", "Allows the user to use shift-right-click to mend items", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "inventory_totem", "Uses a totem from anywhere in the user's inventory on death", PermissionDefault.FALSE, purpur); ++ ++ Permission anvil = DefaultPermissions.registerPermission(PREFIX + "anvil", "Allows the user to use all anvil color and format abilities", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.color", "Allows the user to use color codes in an anvil", PermissionDefault.FALSE, anvil); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.minimessage", "Allows the user to use minimessage tags in an anvil", PermissionDefault.FALSE, anvil); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.remove_italics", "Allows the user to remove italics in an anvil", PermissionDefault.FALSE, anvil); ++ DefaultPermissions.registerPermission(PREFIX + "anvil.format", "Allows the user to use format codes in an anvil", PermissionDefault.FALSE, anvil); ++ anvil.recalculatePermissibles(); ++ ++ Permission book = DefaultPermissions.registerPermission(PREFIX + "book", "Allows the user to use color codes on books", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "book.color.edit", "Allows the user to use color codes on books when editing", PermissionDefault.FALSE, book); ++ DefaultPermissions.registerPermission(PREFIX + "book.color.sign", "Allows the user to use color codes on books when signing", PermissionDefault.FALSE, book); ++ book.recalculatePermissibles(); ++ ++ Permission sign = DefaultPermissions.registerPermission(PREFIX + "sign", "Allows the user to use all sign abilities", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission(PREFIX + "sign.edit", "Allows the user to click signs to open sign editor", PermissionDefault.FALSE, sign); ++ DefaultPermissions.registerPermission(PREFIX + "sign.color", "Allows the user to use color codes on signs", PermissionDefault.FALSE, sign); ++ DefaultPermissions.registerPermission(PREFIX + "sign.style", "Allows the user to use style codes on signs", PermissionDefault.FALSE, sign); ++ DefaultPermissions.registerPermission(PREFIX + "sign.magic", "Allows the user to use magic/obfuscate code on signs", PermissionDefault.FALSE, sign); ++ sign.recalculatePermissibles(); ++ ++ Permission ride = DefaultPermissions.registerPermission("allow.ride", "Allows the user to ride all mobs", PermissionDefault.FALSE, purpur); ++ for (String mob : mobs) { ++ DefaultPermissions.registerPermission("allow.ride." + mob, "Allows the user to ride " + mob, PermissionDefault.FALSE, ride); ++ } ++ ride.recalculatePermissibles(); ++ ++ Permission special = DefaultPermissions.registerPermission("allow.special", "Allows the user to use all mobs special abilities", PermissionDefault.FALSE, purpur); ++ for (String mob : mobs) { ++ DefaultPermissions.registerPermission("allow.special." + mob, "Allows the user to use " + mob + " special ability", PermissionDefault.FALSE, special); ++ } ++ special.recalculatePermissibles(); ++ ++ Permission powered = DefaultPermissions.registerPermission("allow.powered", "Allows the user to toggle all mobs powered state", PermissionDefault.FALSE, purpur); ++ DefaultPermissions.registerPermission("allow.powered.creeper", "Allows the user to toggle creeper powered state", PermissionDefault.FALSE, powered); ++ powered.recalculatePermissibles(); ++ ++ DefaultPermissions.registerPermission(PREFIX + "portal.instant", "Allows the user to bypass portal wait time", PermissionDefault.FALSE, purpur); ++ ++ purpur.recalculatePermissibles(); ++ return purpur; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java b/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bc590c4d49d32f4365a50ceb5785e798702a8179 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java +@@ -0,0 +1,130 @@ ++package org.purpurmc.purpur.event; ++ ++import com.google.common.base.Preconditions; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * This event is called whenever someone runs a command ++ */ ++public class ExecuteCommandEvent extends Event implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancel = false; ++ private CommandSender sender; ++ private Command command; ++ private String label; ++ private String[] args; ++ ++ public ExecuteCommandEvent(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @Nullable String[] args) { ++ this.sender = sender; ++ this.command = command; ++ this.label = label; ++ this.args = args; ++ } ++ ++ /** ++ * Gets the command that the player is attempting to execute. ++ * ++ * @return Command the player is attempting to execute ++ */ ++ @NotNull ++ public Command getCommand() { ++ return command; ++ } ++ ++ /** ++ * Sets the command that the player will execute. ++ * ++ * @param command New command that the player will execute ++ * @throws IllegalArgumentException if command is null or empty ++ */ ++ public void setCommand(@NotNull Command command) throws IllegalArgumentException { ++ Preconditions.checkArgument(command != null, "Command cannot be null"); ++ this.command = command; ++ } ++ ++ /** ++ * Gets the sender that this command will be executed as. ++ * ++ * @return Sender this command will be executed as ++ */ ++ @NotNull ++ public CommandSender getSender() { ++ return sender; ++ } ++ ++ /** ++ * Sets the sender that this command will be executed as. ++ * ++ * @param sender New sender which this event will execute as ++ * @throws IllegalArgumentException if the sender provided is null ++ */ ++ public void setSender(@NotNull final CommandSender sender) throws IllegalArgumentException { ++ Preconditions.checkArgument(sender != null, "Sender cannot be null"); ++ this.sender = sender; ++ } ++ ++ /** ++ * Get the label used to execute this command ++ * ++ * @return Label used to execute this command ++ */ ++ @NotNull ++ public String getLabel() { ++ return label; ++ } ++ ++ /** ++ * Set the label used to execute this command ++ * ++ * @param label Label used ++ */ ++ public void setLabel(@NotNull String label) { ++ this.label = label; ++ } ++ ++ /** ++ * Get the args passed to the command ++ * ++ * @return Args passed to the command ++ */ ++ @NotNull ++ public String[] getArgs() { ++ return args; ++ } ++ ++ /** ++ * Set the args passed to the command ++ * ++ * @param args Args passed to the command ++ */ ++ public void setArgs(@NotNull String[] args) { ++ this.args = args; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java b/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..25e92af7710316ed2afedf846a59dbd672869b51 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java +@@ -0,0 +1,70 @@ ++package org.purpurmc.purpur.event; ++ ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.player.PlayerEvent; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++public class PlayerAFKEvent extends PlayerEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private final boolean setAfk; ++ private boolean shouldKick; ++ private String broadcast; ++ private boolean cancel; ++ ++ public PlayerAFKEvent(@NotNull Player player, boolean setAfk, boolean shouldKick, @Nullable String broadcast, boolean async) { ++ super(player, async); ++ this.setAfk = setAfk; ++ this.shouldKick = shouldKick; ++ this.broadcast = broadcast; ++ } ++ ++ /** ++ * Whether player is going afk or coming back ++ * ++ * @return True if going afk. False is coming back ++ */ ++ public boolean isGoingAfk() { ++ return setAfk; ++ } ++ ++ public boolean shouldKick() { ++ return shouldKick; ++ } ++ ++ public void setShouldKick(boolean shouldKick) { ++ this.shouldKick = shouldKick; ++ } ++ ++ @Nullable ++ public String getBroadcastMsg() { ++ return broadcast; ++ } ++ ++ public void setBroadcastMsg(@Nullable String broadcast) { ++ this.broadcast = broadcast; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/PlayerSetSpawnerTypeWithEggEvent.java b/src/main/java/org/purpurmc/purpur/event/PlayerSetSpawnerTypeWithEggEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..519809eab5d926dc7b0a7bad5d446d0defc099dc +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/PlayerSetSpawnerTypeWithEggEvent.java +@@ -0,0 +1,85 @@ ++package org.purpurmc.purpur.event; ++ ++import org.bukkit.block.Block; ++import org.bukkit.block.CreatureSpawner; ++import org.bukkit.entity.EntityType; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.player.PlayerEvent; ++import org.jetbrains.annotations.NotNull; ++ ++public class PlayerSetSpawnerTypeWithEggEvent extends PlayerEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Block block; ++ private final CreatureSpawner spawner; ++ private EntityType type; ++ private boolean cancel; ++ ++ public PlayerSetSpawnerTypeWithEggEvent(@NotNull Player player, @NotNull Block block, @NotNull CreatureSpawner spawner, @NotNull EntityType type) { ++ super(player); ++ this.block = block; ++ this.spawner = spawner; ++ this.type = type; ++ } ++ ++ /** ++ * Get the spawner Block in the world ++ * ++ * @return Spawner Block ++ */ ++ @NotNull ++ public Block getBlock() { ++ return block; ++ } ++ ++ /** ++ * Get the spawner state ++ * ++ * @return Spawner state ++ */ ++ @NotNull ++ public CreatureSpawner getSpawner() { ++ return spawner; ++ } ++ ++ /** ++ * Gets the EntityType being set on the spawner ++ * ++ * @return EntityType being set ++ */ ++ @NotNull ++ public EntityType getEntityType() { ++ return type; ++ } ++ ++ /** ++ * Sets the EntityType being set on the spawner ++ * ++ * @param type EntityType to set ++ */ ++ public void setEntityType(@NotNull EntityType type) { ++ this.type = type; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/PreBlockExplodeEvent.java b/src/main/java/org/purpurmc/purpur/event/PreBlockExplodeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7db0db7f3afbccdb07390d1bcada109e9e6b30b +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/PreBlockExplodeEvent.java +@@ -0,0 +1,52 @@ ++package org.purpurmc.purpur.event; ++ ++import org.bukkit.block.Block; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.block.BlockExplodeEvent; ++import org.jetbrains.annotations.NotNull; ++import java.util.Collections; ++ ++/** ++ * Called before a block's explosion is processed ++ */ ++public class PreBlockExplodeEvent extends BlockExplodeEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancelled; ++ private final float yield; ++ ++ public PreBlockExplodeEvent(@NotNull final Block what, final float yield) { ++ super(what, Collections.emptyList(), yield); ++ this.yield = yield; ++ this.cancelled = false; ++ } ++ ++ /** ++ * Returns the percentage of blocks to drop from this explosion ++ * ++ * @return The yield. ++ */ ++ public float getYield() { ++ return yield; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return this.cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancelled = cancel; ++ } ++ ++ @Override ++ public @NotNull HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/BeeFoundFlowerEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/BeeFoundFlowerEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..833f46d1941f377765132fc528c45567ee0290d2 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/BeeFoundFlowerEvent.java +@@ -0,0 +1,48 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Bee; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Called when a bee targets a flower ++ */ ++public class BeeFoundFlowerEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Location location; ++ ++ public BeeFoundFlowerEvent(@NotNull Bee bee, @Nullable Location location) { ++ super(bee); ++ this.location = location; ++ } ++ ++ @Override ++ @NotNull ++ public Bee getEntity() { ++ return (Bee) super.getEntity(); ++ } ++ ++ /** ++ * Returns the location of the flower that the bee targets ++ * ++ * @return The location of the flower ++ */ ++ @Nullable ++ public Location getLocation() { ++ return location; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/BeeStartedPollinatingEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/BeeStartedPollinatingEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ae0bb654745724889c67fae9072ae90ea3778ba4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/BeeStartedPollinatingEvent.java +@@ -0,0 +1,47 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Bee; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a bee starts pollinating ++ */ ++public class BeeStartedPollinatingEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Location location; ++ ++ public BeeStartedPollinatingEvent(@NotNull Bee bee, @NotNull Location location) { ++ super(bee); ++ this.location = location; ++ } ++ ++ @Override ++ @NotNull ++ public Bee getEntity() { ++ return (Bee) super.getEntity(); ++ } ++ ++ /** ++ * Returns the location of the flower that the bee pollinates ++ * ++ * @return The location of the flower ++ */ ++ @NotNull ++ public Location getLocation() { ++ return this.location; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/BeeStopPollinatingEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/BeeStopPollinatingEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ff3c9f075be2f624af8b0ce5fffc5ea69a41f32e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/BeeStopPollinatingEvent.java +@@ -0,0 +1,60 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Bee; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Called when a bee stops pollinating ++ */ ++public class BeeStopPollinatingEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Location location; ++ private final boolean success; ++ ++ public BeeStopPollinatingEvent(@NotNull Bee bee, @Nullable Location location, boolean success) { ++ super(bee); ++ this.location = location; ++ this.success = success; ++ } ++ ++ @Override ++ @NotNull ++ public Bee getEntity() { ++ return (Bee) super.getEntity(); ++ } ++ ++ /** ++ * Returns the location of the flower that the bee stopped pollinating ++ * ++ * @return The location of the flower ++ */ ++ @Nullable ++ public Location getLocation() { ++ return location; ++ } ++ ++ /** ++ * Returns whether the bee successfully pollinated the flower ++ * ++ * @return True if the pollination was successful ++ */ ++ public boolean wasSuccessful() { ++ return success; ++ } ++ ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/EntityTeleportHinderedEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/EntityTeleportHinderedEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c66eb163877e872f234d86dc244cab7efeb818cd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/EntityTeleportHinderedEvent.java +@@ -0,0 +1,117 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Fired when an entity is hindered from teleporting. ++ */ ++public class EntityTeleportHinderedEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ @NotNull ++ private final Reason reason; ++ ++ @Nullable ++ private final TeleportCause teleportCause; ++ ++ private boolean retry = false; ++ ++ public EntityTeleportHinderedEvent(@NotNull Entity what, @NotNull Reason reason, ++ @Nullable TeleportCause teleportCause) { ++ super(what); ++ this.reason = reason; ++ this.teleportCause = teleportCause; ++ } ++ ++ /** ++ * @return why the teleport was hindered. ++ */ ++ @NotNull ++ public Reason getReason() { ++ return reason; ++ } ++ ++ /** ++ * @return why the teleport occurred if cause was given, otherwise {@code null}. ++ */ ++ @Nullable ++ public TeleportCause getTeleportCause() { ++ return teleportCause; ++ } ++ ++ /** ++ * Whether the teleport should be retried. ++ *

++ * Note that this can put the server in a never-ending loop of trying to teleport someone resulting in a stack ++ * overflow. Do not retry more than necessary. ++ *

++ * ++ * @return whether the teleport should be retried. ++ */ ++ public boolean shouldRetry() { ++ return retry; ++ } ++ ++ /** ++ * Sets whether the teleport should be retried. ++ *

++ * Note that this can put the server in a never-ending loop of trying to teleport someone resulting in a stack ++ * overflow. Do not retry more than necessary. ++ *

++ * ++ * @param retry whether the teleport should be retried. ++ */ ++ public void setShouldRetry(boolean retry) { ++ this.retry = retry; ++ } ++ ++ /** ++ * Calls the event and tests if should retry. ++ * ++ * @return whether the teleport should be retried. ++ */ ++ @Override ++ public boolean callEvent() { ++ super.callEvent(); ++ return shouldRetry(); ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++ ++ /** ++ * Reason for hindrance in teleports. ++ */ ++ public enum Reason { ++ /** ++ * The teleported entity is a passenger of another entity. ++ */ ++ IS_PASSENGER, ++ ++ /** ++ * The teleported entity has passengers. ++ */ ++ IS_VEHICLE, ++ ++ /** ++ * The teleport event was cancelled. ++ *

++ * This is only caused by players teleporting. ++ *

++ */ ++ EVENT_CANCELLED, ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/GoatRamEntityEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/GoatRamEntityEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f62c14f3d4999e9112c1c73642aa337d97b94b5a +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/GoatRamEntityEvent.java +@@ -0,0 +1,59 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Goat; ++import org.bukkit.entity.LivingEntity; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a goat rams an entity ++ */ ++public class GoatRamEntityEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private final LivingEntity rammedEntity; ++ private boolean cancelled; ++ ++ public GoatRamEntityEvent(@NotNull Goat goat, @NotNull LivingEntity rammedEntity) { ++ super(goat); ++ this.rammedEntity = rammedEntity; ++ } ++ ++ /** ++ * Returns the entity that was rammed by the goat ++ * ++ * @return The rammed entity ++ */ ++ @NotNull ++ public LivingEntity getRammedEntity() { ++ return this.rammedEntity; ++ } ++ ++ @Override ++ @NotNull ++ public Goat getEntity() { ++ return (Goat) super.getEntity(); ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return this.cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancelled = cancel; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/LlamaJoinCaravanEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/LlamaJoinCaravanEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8849bb0becb16db907fa648cca2e98ab9d957c75 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/LlamaJoinCaravanEvent.java +@@ -0,0 +1,61 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Llama; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a Llama tries to join a caravan. ++ *

++ * Cancelling the event will not let the Llama join. To prevent future attempts ++ * at joining a caravan use {@link Llama#setShouldJoinCaravan(boolean)}. ++ */ ++public class LlamaJoinCaravanEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean canceled; ++ private final Llama head; ++ ++ public LlamaJoinCaravanEvent(@NotNull Llama llama, @NotNull Llama head) { ++ super(llama); ++ this.head = head; ++ } ++ ++ @Override ++ @NotNull ++ public Llama getEntity() { ++ return (Llama) entity; ++ } ++ ++ /** ++ * Get the Llama that this Llama is about to follow ++ * ++ * @return Llama about to be followed ++ */ ++ @NotNull ++ public Llama getHead() { ++ return head; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return canceled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ canceled = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/LlamaLeaveCaravanEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/LlamaLeaveCaravanEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c268c35b541a222d50875c29770c846a8ffcc4f8 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/LlamaLeaveCaravanEvent.java +@@ -0,0 +1,34 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Llama; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a Llama leaves a caravan ++ */ ++public class LlamaLeaveCaravanEvent extends EntityEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ public LlamaLeaveCaravanEvent(@NotNull Llama llama) { ++ super(llama); ++ } ++ ++ @Override ++ @NotNull ++ public Llama getEntity() { ++ return (Llama) entity; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/MonsterEggSpawnEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/MonsterEggSpawnEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..82f8a0ea22f07954d516935fc9f73f6aa0f65aa6 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/MonsterEggSpawnEvent.java +@@ -0,0 +1,67 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.HumanEntity; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++public class MonsterEggSpawnEvent extends Event implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean canceled; ++ ++ private final Player player; ++ private Entity entity; ++ private final ItemStack item; ++ ++ public MonsterEggSpawnEvent(@Nullable HumanEntity player, @NotNull Entity entity, @NotNull ItemStack item) { ++ this.player = (Player) player; ++ this.entity = entity; ++ this.item = item; ++ } ++ ++ @Nullable ++ public Player getPlayer() { ++ return player; ++ } ++ ++ @NotNull ++ public Entity getEntity() { ++ return entity; ++ } ++ ++ public void setEntity(@Nullable Entity entity) { ++ if (entity == null) { ++ canceled = true; ++ return; ++ } ++ this.entity = entity; ++ } ++ ++ @NotNull ++ public ItemStack getItem() { ++ return item; ++ } ++ ++ public boolean isCancelled() { ++ return canceled; ++ } ++ ++ public void setCancelled(boolean cancel) { ++ canceled = cancel; ++ } ++ ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/PreEntityExplodeEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/PreEntityExplodeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2d4f68228861492baaea0bcc604dfef623b337ba +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/PreEntityExplodeEvent.java +@@ -0,0 +1,64 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityExplodeEvent; ++import org.jetbrains.annotations.NotNull; ++import java.util.Collections; ++ ++/** ++ * Called before an entity's explosion is processed ++ */ ++public class PreEntityExplodeEvent extends EntityExplodeEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancelled; ++ private final float yield; ++ private final Location location; ++ ++ public PreEntityExplodeEvent(@NotNull org.bukkit.entity.Entity what, @NotNull final Location location, final float yield) { ++ super(what, location, Collections.emptyList(), yield); ++ this.cancelled = false; ++ this.yield = yield; ++ this.location = location; ++ } ++ ++ /** ++ * Returns the percentage of blocks to drop from this explosion ++ * ++ * @return The yield. ++ */ ++ public float getYield() { ++ return yield; ++ } ++ ++ /** ++ * Returns the location where the explosion happened. ++ * ++ * @return The location of the explosion ++ */ ++ @NotNull ++ public Location getLocation() { ++ return location; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return this.cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancelled = cancel; ++ } ++ ++ @Override ++ public @NotNull HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/RidableMoveEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/RidableMoveEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a037df01b07af9ffb98b67aca412c1d34fade03b +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/RidableMoveEvent.java +@@ -0,0 +1,103 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import com.google.common.base.Preconditions; ++import org.bukkit.Location; ++import org.bukkit.entity.Mob; ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Triggered when a ridable mob moves with a rider ++ */ ++public class RidableMoveEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean canceled; ++ private final Player rider; ++ private Location from; ++ private Location to; ++ ++ public RidableMoveEvent(@NotNull Mob entity, @NotNull Player rider, @NotNull Location from, @NotNull Location to) { ++ super(entity); ++ this.rider = rider; ++ this.from = from; ++ this.to = to; ++ } ++ ++ @Override ++ @NotNull ++ public Mob getEntity() { ++ return (Mob) entity; ++ } ++ ++ @NotNull ++ public Player getRider() { ++ return rider; ++ } ++ ++ public boolean isCancelled() { ++ return canceled; ++ } ++ ++ public void setCancelled(boolean cancel) { ++ canceled = cancel; ++ } ++ ++ /** ++ * Gets the location this entity moved from ++ * ++ * @return Location the entity moved from ++ */ ++ @NotNull ++ public Location getFrom() { ++ return from; ++ } ++ ++ /** ++ * Sets the location to mark as where the entity moved from ++ * ++ * @param from New location to mark as the entity's previous location ++ */ ++ public void setFrom(@NotNull Location from) { ++ validateLocation(from); ++ this.from = from; ++ } ++ ++ /** ++ * Gets the location this entity moved to ++ * ++ * @return Location the entity moved to ++ */ ++ @NotNull ++ public Location getTo() { ++ return to; ++ } ++ ++ /** ++ * Sets the location that this entity will move to ++ * ++ * @param to New Location this entity will move to ++ */ ++ public void setTo(@NotNull Location to) { ++ validateLocation(to); ++ this.to = to; ++ } ++ ++ private void validateLocation(@NotNull Location loc) { ++ Preconditions.checkArgument(loc != null, "Cannot use null location!"); ++ Preconditions.checkArgument(loc.getWorld() != null, "Cannot use null location with null world!"); ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/entity/RidableSpacebarEvent.java b/src/main/java/org/purpurmc/purpur/event/entity/RidableSpacebarEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3d3a7d898e3278ce998d713dafbb4b354dad7fc7 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/entity/RidableSpacebarEvent.java +@@ -0,0 +1,37 @@ ++package org.purpurmc.purpur.event.entity; ++ ++import org.bukkit.entity.Entity; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.entity.EntityEvent; ++import org.jetbrains.annotations.NotNull; ++ ++public class RidableSpacebarEvent extends EntityEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ private boolean cancelled; ++ ++ public RidableSpacebarEvent(@NotNull Entity entity) { ++ super(entity); ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancelled; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ cancelled = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/inventory/AnvilTakeResultEvent.java b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilTakeResultEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b363c91a29f826910db22f2643decf996a067ab5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilTakeResultEvent.java +@@ -0,0 +1,52 @@ ++package org.purpurmc.purpur.event.inventory; ++ ++import org.bukkit.entity.HumanEntity; ++import org.bukkit.entity.Player; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.inventory.InventoryEvent; ++import org.bukkit.inventory.AnvilInventory; ++import org.bukkit.inventory.InventoryView; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a player takes the result item out of an anvil ++ */ ++public class AnvilTakeResultEvent extends InventoryEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Player player; ++ private final ItemStack result; ++ ++ public AnvilTakeResultEvent(@NotNull HumanEntity player, @NotNull InventoryView view, @NotNull ItemStack result) { ++ super(view); ++ this.player = (Player) player; ++ this.result = result; ++ } ++ ++ @NotNull ++ public Player getPlayer() { ++ return player; ++ } ++ ++ @NotNull ++ public ItemStack getResult() { ++ return result; ++ } ++ ++ @NotNull ++ @Override ++ public AnvilInventory getInventory() { ++ return (AnvilInventory) super.getInventory(); ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/inventory/AnvilUpdateResultEvent.java b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilUpdateResultEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fd6a5a3589d436c2aaf988fd305899695799d3bb +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/inventory/AnvilUpdateResultEvent.java +@@ -0,0 +1,35 @@ ++package org.purpurmc.purpur.event.inventory; ++ ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.inventory.InventoryEvent; ++import org.bukkit.inventory.AnvilInventory; ++import org.bukkit.inventory.InventoryView; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when anvil slots change, triggering the result slot to be updated ++ */ ++public class AnvilUpdateResultEvent extends InventoryEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ public AnvilUpdateResultEvent(@NotNull InventoryView view) { ++ super(view); ++ } ++ ++ @NotNull ++ @Override ++ public AnvilInventory getInventory() { ++ return (AnvilInventory) super.getInventory(); ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/inventory/GrindstoneTakeResultEvent.java b/src/main/java/org/purpurmc/purpur/event/inventory/GrindstoneTakeResultEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eebb5d124456b8209d1b8e8cc4cb772dd3714f04 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/inventory/GrindstoneTakeResultEvent.java +@@ -0,0 +1,72 @@ ++package org.purpurmc.purpur.event.inventory; ++ ++import org.bukkit.entity.HumanEntity; ++import org.bukkit.entity.Player; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.inventory.InventoryEvent; ++import org.bukkit.inventory.GrindstoneInventory; ++import org.bukkit.inventory.InventoryView; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a player takes the result item out of a Grindstone ++ */ ++public class GrindstoneTakeResultEvent extends InventoryEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final Player player; ++ private final ItemStack result; ++ private int experienceAmount; ++ ++ public GrindstoneTakeResultEvent(@NotNull HumanEntity player, @NotNull InventoryView view, @NotNull ItemStack result, int experienceAmount) { ++ super(view); ++ this.player = (Player) player; ++ this.result = result; ++ this.experienceAmount = experienceAmount; ++ } ++ ++ @NotNull ++ public Player getPlayer() { ++ return player; ++ } ++ ++ @NotNull ++ public ItemStack getResult() { ++ return result; ++ } ++ ++ @NotNull ++ @Override ++ public GrindstoneInventory getInventory() { ++ return (GrindstoneInventory) super.getInventory(); ++ } ++ ++ /** ++ * Get the amount of experience this transaction will give ++ * ++ * @return Amount of experience to give ++ */ ++ public int getExperienceAmount() { ++ return this.experienceAmount; ++ } ++ ++ /** ++ * Set the amount of experience this transaction will give ++ * ++ * @param experienceAmount Amount of experience to give ++ */ ++ public void setExperienceAmount(int experienceAmount) { ++ this.experienceAmount = experienceAmount; ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/packet/NetworkItemSerializeEvent.java b/src/main/java/org/purpurmc/purpur/event/packet/NetworkItemSerializeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0da73d2ea83a6055e34894ba1c7506fc8667712 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/packet/NetworkItemSerializeEvent.java +@@ -0,0 +1,48 @@ ++package org.purpurmc.purpur.event.packet; ++ ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Called when an item is about to be written to a packet. ++ */ ++public class NetworkItemSerializeEvent extends Event { ++ private ItemStack itemStack; ++ ++ public NetworkItemSerializeEvent(@NotNull ItemStack itemStack) { ++ super(!org.bukkit.Bukkit.isPrimaryThread()); ++ this.itemStack = itemStack; ++ } ++ ++ /** ++ * @return The item that is about to be serialized. Not mutable ++ */ ++ @NotNull ++ public ItemStack getItemStack() { ++ return itemStack; ++ } ++ ++ /** ++ * Sets the item that will be serialized. ++ * ++ * @param itemStack The item ++ */ ++ public void setItemStack(@Nullable ItemStack itemStack) { ++ this.itemStack = itemStack; ++ } ++ ++ private static final HandlerList handlers = new HandlerList(); ++ ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/event/player/PlayerBookTooLargeEvent.java b/src/main/java/org/purpurmc/purpur/event/player/PlayerBookTooLargeEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c88394336bc9ab0f66a2af24d393f4a176a234d5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/event/player/PlayerBookTooLargeEvent.java +@@ -0,0 +1,65 @@ ++package org.purpurmc.purpur.event.player; ++ ++import org.bukkit.Bukkit; ++import org.bukkit.entity.Player; ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.player.PlayerEvent; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Called when a player tries to bypass book limitations ++ */ ++public class PlayerBookTooLargeEvent extends PlayerEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ private final ItemStack book; ++ private boolean kickPlayer = true; ++ ++ /** ++ * @param player The player ++ * @param book The book ++ */ ++ public PlayerBookTooLargeEvent(@NotNull Player player, @NotNull ItemStack book) { ++ super(player, !Bukkit.isPrimaryThread()); ++ this.book = book; ++ } ++ ++ /** ++ * Get the book containing the wanted edits ++ * ++ * @return The book ++ */ ++ @NotNull ++ public ItemStack getBook() { ++ return book; ++ } ++ ++ /** ++ * Whether server should kick the player or not ++ * ++ * @return True to kick player ++ */ ++ public boolean shouldKickPlayer() { ++ return kickPlayer; ++ } ++ ++ /** ++ * Whether server should kick the player or not ++ * ++ * @param kickPlayer True to kick player ++ */ ++ public void setShouldKickPlayer(boolean kickPlayer) { ++ this.kickPlayer = kickPlayer; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/language/Language.java b/src/main/java/org/purpurmc/purpur/language/Language.java +new file mode 100644 +index 0000000000000000000000000000000000000000..38483d908ed830e97883733bee2370f87060f4c7 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/language/Language.java +@@ -0,0 +1,60 @@ ++package org.purpurmc.purpur.language; ++ ++import net.kyori.adventure.translation.Translatable; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Represents a language that can translate translation keys ++ */ ++public abstract class Language { ++ private static Language language; ++ ++ /** ++ * Returns the default language of the server ++ */ ++ @NotNull ++ public static Language getLanguage() { ++ return language; ++ } ++ ++ public static void setLanguage(@NotNull Language language) { ++ if (Language.language != null) { ++ throw new UnsupportedOperationException("Cannot redefine singleton Language"); ++ } ++ Language.language = language; ++ } ++ ++ /** ++ * Checks if a certain translation key is translatable with this language ++ * @param key The translation key ++ * @return Whether this language can translate the key ++ */ ++ abstract public boolean has(@NotNull String key); ++ ++ /** ++ * Checks if a certain translation key is translatable with this language ++ * @param key The translation key ++ * @return Whether this language can translate the key ++ */ ++ public boolean has(@NotNull Translatable key) { ++ return has(key.translationKey()); ++ } ++ ++ /** ++ * Translates a translation key to this language ++ * @param key The translation key ++ * @return The translated key, or the translation key if it couldn't be translated ++ */ ++ @NotNull ++ abstract public String getOrDefault(@NotNull String key); ++ ++ /** ++ * Translates a translation key to this language ++ * @param key The translation key ++ * @return The translated key, or the translation key if it couldn't be translated ++ */ ++ @NotNull ++ public String getOrDefault(@NotNull Translatable key) { ++ return getOrDefault(key.translationKey()); ++ } ++} +diff --git a/src/main/java/org/spigotmc/CustomTimingsHandler.java b/src/main/java/org/spigotmc/CustomTimingsHandler.java +index 12946bd55fcf7c40d39081779a7fa30049ee6165..9c2d605c50cbf9aefa56ec209df9f6cea1392e89 100644 +--- a/src/main/java/org/spigotmc/CustomTimingsHandler.java ++++ b/src/main/java/org/spigotmc/CustomTimingsHandler.java +@@ -61,7 +61,7 @@ public final class CustomTimingsHandler { + handler = timing; + } + +- public void startTiming() { handler.startTiming(); } +- public void stopTiming() { handler.stopTiming(); } ++ public void startTiming() { /*handler.startTiming();*/ } // Purpur ++ public void stopTiming() { /*handler.stopTiming();*/ } // Purpur + + } +diff --git a/src/test/java/org/bukkit/AnnotationTest.java b/src/test/java/org/bukkit/AnnotationTest.java +index 06e96e5c98f1a7a68c8b4b5e527314c1aa774e38..49bba9a7a02b9cf3a552583315eff2b7dbe060c0 100644 +--- a/src/test/java/org/bukkit/AnnotationTest.java ++++ b/src/test/java/org/bukkit/AnnotationTest.java +@@ -47,6 +47,10 @@ public class AnnotationTest { + "org/bukkit/plugin/java/PluginClassLoader", + // Generic functional interface + "org/bukkit/util/Consumer", ++ // Purpur start ++ "gg/pufferfish/pufferfish/sentry/SentryContext", ++ "gg/pufferfish/pufferfish/sentry/SentryContext$State", ++ // Purpur end + // Paper start + "io/papermc/paper/util/TransformingRandomAccessList", + "io/papermc/paper/util/TransformingRandomAccessList$TransformedListIterator", diff --git a/patches/api/0002-Leaf-config-files.patch b/patches/api/0003-Leaf-config-files.patch similarity index 79% rename from patches/api/0002-Leaf-config-files.patch rename to patches/api/0003-Leaf-config-files.patch index adeac36f..8de06bbb 100644 --- a/patches/api/0002-Leaf-config-files.patch +++ b/patches/api/0003-Leaf-config-files.patch @@ -5,12 +5,12 @@ Subject: [PATCH] Leaf config files diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 6253e761c595c8b89d08f9d42fe3e19cadbf4918..66740fa8e37204c3f7f9275b45f398d80f2ff44e 100644 +index bb4452abd1e6b169d03e77a6b6bdec813f300e1d..d772a989c4bd2d7540f3f41c4576b36835d26f12 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java -@@ -2021,6 +2021,14 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi - - // Paper end +@@ -2033,6 +2033,14 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + } + // Purpur end + // Leaf start + @NotNull diff --git a/patches/api/0003-Bump-Dependencies.patch b/patches/api/0004-Bump-Dependencies.patch similarity index 95% rename from patches/api/0003-Bump-Dependencies.patch rename to patches/api/0004-Bump-Dependencies.patch index ad955625..3149280f 100644 --- a/patches/api/0003-Bump-Dependencies.patch +++ b/patches/api/0004-Bump-Dependencies.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Bump Dependencies diff --git a/build.gradle.kts b/build.gradle.kts -index e23ed86c4288ecdfef37bf5b2cd132f348bf852a..733236d0cde112d99b017f67f44d982396ffa352 100644 +index 3e148458cc78a3225e8d6572b43e1d358791eec2..8813d67ff64f4f213de223c17cd16ef21aa9c916 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,15 +24,17 @@ configurations.api { @@ -79,7 +79,7 @@ index e23ed86c4288ecdfef37bf5b2cd132f348bf852a..733236d0cde112d99b017f67f44d9823 } configure { -@@ -113,9 +117,11 @@ tasks.withType { +@@ -115,9 +119,11 @@ tasks.withType { options.use() options.isDocFilesSubDirs = true options.links( @@ -92,7 +92,7 @@ index e23ed86c4288ecdfef37bf5b2cd132f348bf852a..733236d0cde112d99b017f67f44d9823 // Paper start //"https://javadoc.io/doc/net.md-5/bungeecord-chat/1.16-R0.4/", // don't link to bungee chat "https://jd.advntr.dev/api/$adventureVersion/", -@@ -156,6 +162,9 @@ val scanJar = tasks.register("scanJarForBadCalls", io.papermc.paperweight.tasks. +@@ -158,6 +164,9 @@ val scanJar = tasks.register("scanJarForBadCalls", io.papermc.paperweight.tasks. jarToScan.set(tasks.jar.flatMap { it.archiveFile }) classpath.from(configurations.compileClasspath) } diff --git a/patches/api/0004-KTP-Optimize-Spigot-event-bus.patch b/patches/api/0005-KTP-Optimize-Spigot-event-bus.patch similarity index 100% rename from patches/api/0004-KTP-Optimize-Spigot-event-bus.patch rename to patches/api/0005-KTP-Optimize-Spigot-event-bus.patch diff --git a/patches/api/0005-KeYi-Player-Skull-API.patch b/patches/api/0006-KeYi-Player-Skull-API.patch similarity index 82% rename from patches/api/0005-KeYi-Player-Skull-API.patch rename to patches/api/0006-KeYi-Player-Skull-API.patch index 0d96b328..900557f0 100644 --- a/patches/api/0005-KeYi-Player-Skull-API.patch +++ b/patches/api/0006-KeYi-Player-Skull-API.patch @@ -7,7 +7,7 @@ Original license: MIT Original project: https://github.com/KeYiMC/KeYi diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java -index af82b2d96080c0657371d80a36192f937c5975bb..c186ffee39328d94f4dfc2d000602f40e829e324 100644 +index f5e2e1bdd31e74fdc00da5b503b178350e8235cd..801ac3e3b75a92121d7f50d9df8e1e227abadbae 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -4,7 +4,10 @@ import java.net.InetSocketAddress; @@ -22,10 +22,10 @@ index af82b2d96080c0657371d80a36192f937c5975bb..c186ffee39328d94f4dfc2d000602f40 import org.bukkit.DyeColor; import org.bukkit.Effect; import org.bukkit.GameMode; -@@ -3047,4 +3050,22 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM - @Override - Spigot spigot(); - // Spigot end +@@ -3182,4 +3185,22 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM + */ + void sendDeathScreen(@NotNull net.kyori.adventure.text.Component message, @Nullable Entity killer); + // Purpur end + + // KeYi start + /** diff --git a/patches/api/0006-Slice-Smooth-Teleports.patch b/patches/api/0007-Slice-Smooth-Teleports.patch similarity index 93% rename from patches/api/0006-Slice-Smooth-Teleports.patch rename to patches/api/0007-Slice-Smooth-Teleports.patch index c74b1864..4930e80d 100644 --- a/patches/api/0006-Slice-Smooth-Teleports.patch +++ b/patches/api/0007-Slice-Smooth-Teleports.patch @@ -7,7 +7,7 @@ Original license: MIT Original project: https://github.com/Cryptite/Slice diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java -index c186ffee39328d94f4dfc2d000602f40e829e324..a236d15a618ec2b9b63b66fb67bb8a86206b43e3 100644 +index 801ac3e3b75a92121d7f50d9df8e1e227abadbae..23b53929b3333bab04bd1aad1873d14e7d6ef54d 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -2932,6 +2932,19 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM diff --git a/patches/server/0001-Rebrand.patch b/patches/server/0001-Rebrand.patch index 0488960a..7e2a979e 100644 --- a/patches/server/0001-Rebrand.patch +++ b/patches/server/0001-Rebrand.patch @@ -40,7 +40,7 @@ index e45e6b44b2a8f2cdae6e0048a812b92126aa17ca..b5f3f213da8a40d5184098af017c8e26 .completer(new ConsoleCommandCompleter(this.server)) .option(LineReader.Option.COMPLETE_IN_WORD, true); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 0782da72420e6988728a699af420fd17e5828e06..ae454d874b6eea828d5491fc585d3dd7fa87ba8f 100644 +index 5db4312ed1973a2395af66975a43abe5beffa1cd..1c671eb71254172fe023eb81cda21976d861a81f 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1030,7 +1030,7 @@ public abstract class MinecraftServer extends MinecraftServerBlockableEventLoop @@ -52,17 +52,8 @@ index 0782da72420e6988728a699af420fd17e5828e06..ae454d874b6eea828d5491fc585d3dd7 // Gale end - branding changes // Gale start - base thread pool while (serverThread.isAlive()) { -@@ -1880,7 +1880,7 @@ public abstract class MinecraftServer extends MinecraftServerBlockableEventLoop - - @DontObfuscate - public String getServerModName() { -- return "Gale"; // Gale - branding changes - Gale > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! -+ return "Leaf"; // Leaf - Leaf > // Gale - branding changes - Gale > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! - } - - public SystemReport fillSystemReport(SystemReport details) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 49ec0bd654e063b21d9673a17ed3df9b9e7f2242..76e96b8a57bb84b92abc7932b8fed2950aaa87bb 100644 +index f903aa3d94505af701bb7f16269f7a25e5ab3044..586bb5f9b98594d29731e52760c74b7390d330d5 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -261,7 +261,7 @@ import javax.annotation.Nullable; // Paper diff --git a/patches/server/0008-Purpur-Base.patch b/patches/server/0008-Purpur-Base.patch new file mode 100644 index 00000000..baafd36a --- /dev/null +++ b/patches/server/0008-Purpur-Base.patch @@ -0,0 +1,98 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Github Actions +Date: Wed, 24 May 2023 06:00:03 +0800 +Subject: [PATCH] Purpur Base + +Original license: MIT +Original project: https://github.com/PurpurMC/Purpur + +diff --git a/build.gradle.kts b/build.gradle.kts +index 493acdeeb56ed13a0c30d9d10b2f9171df296d98..3c1e97fb031f7cdf73ecb6cf8ec662e08b78f96f 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -45,6 +45,10 @@ dependencies { + } + // Paper end + ++ implementation("org.mozilla:rhino-runtime:1.7.14") // Purpur ++ implementation("org.mozilla:rhino-engine:1.7.14") // Purpur ++ implementation("dev.omega24:upnp4j:1.0") // Purpur ++ + runtimeOnly("org.apache.maven:maven-resolver-provider:3.8.5") + runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.3") + runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.3") +@@ -168,7 +172,7 @@ fun TaskContainer.registerRunTask( + } + } + // Gale end - use default Java installation for development runs +- group = "paper" ++ group = "paperweight" // Purpur + mainClass.set("org.bukkit.craftbukkit.Main") + standardInput = System.`in` + workingDir = rootProject.layout.projectDirectory +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +index 4e56018b64d11f76c8da43fd8f85c6de72204e36..aa8212432825db65cf485cd93f734ccd9eefcb5a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +@@ -21,7 +21,12 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + + @Override + public void sendMessage(String message) { +- this.sendRawMessage(message); ++ // Purpur start ++ String[] parts = message.split("\n"); ++ for (String part : parts) { ++ this.sendRawMessage(part); ++ } ++ // Purpur end + } + + @Override +@@ -91,7 +96,7 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + // Paper start + @Override + public void sendMessage(final net.kyori.adventure.identity.Identity identity, final net.kyori.adventure.text.Component message, final net.kyori.adventure.audience.MessageType type) { +- this.sendRawMessage(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(message)); ++ this.sendMessage(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(message)); // Purpur + } + + @Override +diff --git a/src/main/java/org/galemc/gale/version/AbstractPaperVersionFetcher.java b/src/main/java/org/galemc/gale/version/AbstractPaperVersionFetcher.java +index 7fee1c2779ab390586b2d3f75f56890846323500..5fdb227acfd1d8f55b770c8a66e97494c36db33c 100644 +--- a/src/main/java/org/galemc/gale/version/AbstractPaperVersionFetcher.java ++++ b/src/main/java/org/galemc/gale/version/AbstractPaperVersionFetcher.java +@@ -68,7 +68,7 @@ public abstract class AbstractPaperVersionFetcher implements VersionFetcher { + // Gale end - branding changes - version fetcher + final Component history = getHistory(); + +- return history != null ? TextComponent.ofChildren(updateMessage, Component.newline(), history) : updateMessage; ++ return history != null ? Component.join(net.kyori.adventure.text.JoinConfiguration.separator(Component.newline()), history, updateMessage) : updateMessage; // Purpur + } + + protected @Nullable String getMinecraftVersion() { // Gale - branding changes - version fetcher +@@ -120,13 +120,13 @@ public abstract class AbstractPaperVersionFetcher implements VersionFetcher { + + switch (distance) { + case -1: +- return Component.text("Error obtaining version information", NamedTextColor.YELLOW); ++ return Component.text("* Error obtaining version information", NamedTextColor.RED); // Purpur + case 0: +- return Component.text("You are running the latest version", NamedTextColor.GREEN); ++ return Component.text("* You are running the latest version", NamedTextColor.GREEN); // Purpur + case -2: +- return Component.text("Unknown version", NamedTextColor.YELLOW); ++ return Component.text("* Unknown version", NamedTextColor.YELLOW); // Purpur + default: +- return Component.text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW) ++ return Component.text("* You are " + distance + " version(s) behind", NamedTextColor.YELLOW) // Purpur + .append(Component.newline()) + .append(Component.text("Download the new version at: ") + .append(Component.text(this.downloadPage, NamedTextColor.GOLD) // Gale - branding changes - version fetcher +@@ -174,6 +174,6 @@ public abstract class AbstractPaperVersionFetcher implements VersionFetcher { + return null; + } + +- return Component.text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC); ++ return org.bukkit.ChatColor.parseMM("Previous: %s", oldVersion); // Purpur + } + } diff --git a/patches/server/0008-Purpur-Server-Changes.patch b/patches/server/0008-Purpur-Server-Changes.patch new file mode 100644 index 00000000..5637889e --- /dev/null +++ b/patches/server/0008-Purpur-Server-Changes.patch @@ -0,0 +1,27422 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Github Actions +Date: Tue, 23 May 2023 21:25:22 +0000 +Subject: [PATCH] Purpur Server Changes + + +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 692c962193cf9fcc6801fc93f3220bdc673d527b..8cde30544e14f8fc2dac32966ae3c21f8cf3a551 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -593,7 +593,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Pufferfish", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish ++ Metrics metrics = new Metrics("Purpur", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish // Purpur + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -602,16 +602,8 @@ public class Metrics { + })); + + metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); +- metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); +- final String paperVersion; +- final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion(); +- if (implVersion != null) { +- final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1); +- paperVersion = "git-Pufferfish-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); // Pufferfish +- } else { +- paperVersion = "unknown"; +- } +- metrics.addCustomChart(new Metrics.SimplePie("pufferfish_version", () -> paperVersion)); // Pufferfish ++ metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() ? "bungee" : "offline"))); // Purpur ++ metrics.addCustomChart(new Metrics.SimplePie("purpur_version", () -> (org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() != null) ? org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion() : "unknown")); // Purpur + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java +index a08c00b8c0488d18be5e182f7892e5ab71d12247..338f693d098b6ab507c30f6411c9a952c34ba8e3 100644 +--- a/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java ++++ b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java +@@ -136,6 +136,10 @@ public class MobGoalHelper { + static { + // TODO these kinda should be checked on each release, in case obfuscation changes + deobfuscationMap.put("abstract_skeleton_1", "abstract_skeleton_melee"); ++ // Purpur start ++ deobfuscationMap.put("zombie_1", "zombie_attack_villager"); ++ deobfuscationMap.put("drowned_1", "drowned_attack_villager"); ++ // Purpur end + + ignored.add("goal_selector_1"); + ignored.add("goal_selector_2"); +diff --git a/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java b/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java +index fa56cd09102a89692b42f1d14257990508c5c720..f9251183df72ddc56662fd3f02acf21641a2200c 100644 +--- a/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java ++++ b/src/main/java/com/destroystokyo/paper/gui/RAMDetails.java +@@ -58,7 +58,7 @@ public class RAMDetails extends JList { + GraphData data = RAMGraph.DATA.peekLast(); + Vector vector = new Vector<>(); + +- double[] tps = new double[] {server.tps1.getAverage(), server.tps5.getAverage(), server.tps15.getAverage()}; ++ double[] tps = new double[] {server.tps5s.getAverage(), server.tps1.getAverage(), server.tps5.getAverage(), server.tps15.getAverage()}; // Purpur + String[] tpsAvg = new String[tps.length]; + + for ( int g = 0; g < tps.length; g++) { +@@ -67,7 +67,7 @@ public class RAMDetails extends JList { + vector.add("Memory use: " + (data.getUsedMem() / 1024L / 1024L) + " mb (" + (data.getFree() * 100L / data.getMax()) + "% free)"); + vector.add("Heap: " + (data.getTotal() / 1024L / 1024L) + " / " + (data.getMax() / 1024L / 1024L) + " mb"); + vector.add("Avg tick: " + DECIMAL_FORMAT.format(getAverage(server.tickTimes)) + " ms"); +- vector.add("TPS from last 1m, 5m, 15m: " + String.join(", ", tpsAvg)); ++ vector.add("TPS from last 5s, 1m, 5m, 15m: " + String.join(", ", tpsAvg)); // Purpur + + setListData(vector); + } +diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java +index 6e441a1a28ba72a8b1cc09fe5fca71b3c70627d4..47e77541e558e18758ae0fcc2aa4e47261e928b6 100644 +--- a/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java ++++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java +@@ -28,6 +28,7 @@ public class PufferfishConfig { + + private static final YamlFile config = new YamlFile(); + private static int updates = 0; ++ public static File pufferfishFile; // Purpur + + private static ConfigurationSection convertToBukkit(org.simpleyaml.configuration.ConfigurationSection section) { + ConfigurationSection newSection = new MemoryConfiguration(); +@@ -50,7 +51,7 @@ public class PufferfishConfig { + } + + public static void load() throws IOException { +- File configFile = new File("pufferfish.yml"); ++ File configFile = pufferfishFile; // Purpur + + if (configFile.exists()) { + try { +@@ -224,7 +225,7 @@ public class PufferfishConfig { + public static int activationDistanceMod; + + private static void dynamicActivationOfBrains() throws IOException { +- dearEnabled = getBoolean("dab.enabled", "activation-range.enabled", true); ++ dearEnabled = getBoolean("dab.enabled", "activation-range.enabled", false); // Purpur + startDistance = getInt("dab.start-distance", "activation-range.start-distance", 12, + "This value determines how far away an entity has to be", + "from the player to start being effected by DEAR."); +@@ -268,7 +269,7 @@ public class PufferfishConfig { + + public static boolean throttleInactiveGoalSelectorTick; + private static void inactiveGoalSelectorThrottle() { +- getBoolean("inactive-goal-selector-throttle", "inactive-goal-selector-disable", true, ++ getBoolean("inactive-goal-selector-throttle", "inactive-goal-selector-disable", false, // Purpur + "Throttles the AI goal selector in entity inactive ticks.", + "This can improve performance by a few percent, but has minor gameplay implications."); + } +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +index ad3560284ae79b9c6bbc8752be7d9d14b18e226e..37d7d89aac826e5e9c072cc82c64ca9192173e91 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -922,9 +922,9 @@ public final class ChunkHolderManager { + } + + public boolean processTicketUpdates() { +- co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager ++ //co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager // Purpur + return this.processTicketUpdates(true, true, null); +- } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager ++ //} finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager // Purpur + } + + private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +index 8013dd333e27aa5fd0beb431fa32491eec9f5246..e42eb93fd9f6f51ff5bb4b14a2304d4ffcdd8441 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +@@ -1750,7 +1750,7 @@ public final class NewChunkHolder { + boolean canSavePOI = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && (poi != null && poi.isDirty()); + boolean canSaveEntities = entities != null; + +- try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper // Purpur + if (canSaveChunk) { + canSaveChunk = this.saveChunk(chunk, unloading); + } +@@ -1764,7 +1764,7 @@ public final class NewChunkHolder { + this.lastEntityUnload = null; + } + } +- } ++ //} // Purpur + + return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; + } +diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +index f0fce4113fb07c64adbec029d177c236cbdcbae8..e94224ed280247ee69dfdff8dc960f2b8729be33 100644 +--- a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +@@ -78,10 +78,10 @@ public class PaperPluginsCommand extends BukkitCommand { + this.setAliases(Arrays.asList("pl")); + } + +- private static List formatProviders(TreeMap> plugins) { ++ private static List formatProviders(TreeMap> plugins, @NotNull CommandSender sender) { // Purpur + List components = new ArrayList<>(plugins.size()); + for (PluginProvider entry : plugins.values()) { +- components.add(formatProvider(entry)); ++ components.add(formatProvider(entry, sender)); // Purpur + } + + boolean isFirst = true; +@@ -109,7 +109,7 @@ public class PaperPluginsCommand extends BukkitCommand { + return formattedSublists; + } + +- private static Component formatProvider(PluginProvider provider) { ++ private static Component formatProvider(PluginProvider provider, @NotNull CommandSender sender) { // Purpur + TextComponent.Builder builder = Component.text(); + if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) { + builder.append(LEGACY_PLUGIN_STAR); +@@ -117,12 +117,64 @@ public class PaperPluginsCommand extends BukkitCommand { + + String name = provider.getMeta().getName(); + Component pluginName = Component.text(name, fromStatus(provider)) +- .clickEvent(ClickEvent.runCommand("/version " + name)); ++ // Purpur start ++ .clickEvent(ClickEvent.suggestCommand("/version " + name)); ++ ++ if (sender instanceof org.bukkit.entity.Player && sender.hasPermission("bukkit.command.version")) { ++ // Event components ++ String description = provider.getMeta().getDescription(); ++ TextComponent.Builder hover = Component.text(); ++ hover.append(Component.text("Version: ", NamedTextColor.WHITE)).append(Component.text(provider.getMeta().getVersion(), NamedTextColor.GREEN)); ++ ++ if (description != null) { ++ hover.append(Component.newline()) ++ .append(Component.text("Description: ", NamedTextColor.WHITE)) ++ .append(Component.text(description, NamedTextColor.GREEN)); ++ } ++ ++ if (provider.getMeta().getWebsite() != null) { ++ hover.append(Component.newline()) ++ .append(Component.text("Website: ", NamedTextColor.WHITE)) ++ .append(Component.text(provider.getMeta().getWebsite(), NamedTextColor.GREEN)); ++ } ++ ++ if (!provider.getMeta().getAuthors().isEmpty()) { ++ hover.append(Component.newline()); ++ if (provider.getMeta().getAuthors().size() == 1) { ++ hover.append(Component.text("Author: ")); ++ } else { ++ hover.append(Component.text("Authors: ")); ++ } ++ ++ hover.append(getAuthors(provider.getMeta())); ++ } ++ ++ pluginName.hoverEvent(hover.build()); ++ } + + builder.append(pluginName); ++ // Purpur end ++ ++ return builder.build(); ++ } ++ ++ // Purpur start ++ @NotNull ++ private static TextComponent getAuthors(@NotNull final PluginMeta pluginMeta) { ++ TextComponent.Builder builder = Component.text(); ++ List authors = pluginMeta.getAuthors(); ++ ++ for (int i = 0; i < authors.size(); i++) { ++ if (i > 0) { ++ builder.append(Component.text(i < authors.size() - 1 ? ", " : " and ", NamedTextColor.WHITE)); ++ } ++ ++ builder.append(Component.text(authors.get(i), NamedTextColor.GREEN)); ++ } + + return builder.build(); + } ++ // Purpur end + + private static Component asPlainComponents(String strings) { + net.kyori.adventure.text.TextComponent.Builder builder = Component.text(); +@@ -182,24 +234,24 @@ public class PaperPluginsCommand extends BukkitCommand { + } + } + +- Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); ++ //Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); + //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs + +- sender.sendMessage(infoMessage); ++ //sender.sendMessage(infoMessage); // Purpur + + if (!paperPlugins.isEmpty()) { +- sender.sendMessage(PAPER_HEADER); ++ sender.sendMessage(PAPER_HEADER.append(Component.text(" (%s):".formatted(paperPlugins.size())))); // Purpur + } + +- for (Component component : formatProviders(paperPlugins)) { ++ for (Component component : formatProviders(paperPlugins, sender)) { // Purpur + sender.sendMessage(component); + } + + if (!spigotPlugins.isEmpty()) { +- sender.sendMessage(BUKKIT_HEADER); ++ sender.sendMessage(BUKKIT_HEADER.append(Component.text(" (%s):".formatted(spigotPlugins.size())))); // Purpur + } + +- for (Component component : formatProviders(spigotPlugins)) { ++ for (Component component : formatProviders(spigotPlugins, sender)) { // Purpur + sender.sendMessage(component); + } + +diff --git a/src/main/java/io/papermc/paper/console/HexFormattingConverter.java b/src/main/java/io/papermc/paper/console/HexFormattingConverter.java +index b9922b07cb105618390187d98acdf89e728e1f5a..6a1eda942aa33fc0802066416f8bc64f5f15d011 100644 +--- a/src/main/java/io/papermc/paper/console/HexFormattingConverter.java ++++ b/src/main/java/io/papermc/paper/console/HexFormattingConverter.java +@@ -38,6 +38,7 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + private static final String ANSI_RESET = "\u001B[m"; + + private static final char COLOR_CHAR = 0x7f; ++ private static final char LEGACY_CHAR = 0xa7; // Purpur + public static final LegacyComponentSerializer SERIALIZER = LegacyComponentSerializer.builder() + .hexColors() + .flattener(PaperAdventure.FLATTENER) +@@ -49,6 +50,8 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + private static final String RESET_RGB_ANSI = ANSI_RESET + RGB_ANSI; + private static final Pattern NAMED_PATTERN = Pattern.compile(COLOR_CHAR + "[0-9a-fk-orA-FK-OR]"); + private static final Pattern RGB_PATTERN = Pattern.compile(COLOR_CHAR + "#([0-9a-fA-F]){6}"); ++ private static final Pattern LEGACY_RGB_PATTERN = Pattern.compile(LEGACY_CHAR + "x((" + LEGACY_CHAR + "[0-9a-fA-F]){6})"); // Purpur ++ private static final Pattern LEGACY_PATTERN = Pattern.compile(LEGACY_CHAR + "([0-9a-fk-orxA-FK-ORX])"); // Purpur + + private static final String[] RGB_ANSI_CODES = new String[]{ + formatHexAnsi(NamedTextColor.BLACK), // Black §0 +@@ -134,7 +137,21 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + } + + private static String convertRGBColors(final String input) { +- return RGB_PATTERN.matcher(input).replaceAll(result -> { ++ // Purpur start - lets just shove this back in place ++ Matcher matcher = LEGACY_RGB_PATTERN.matcher(input); ++ StringBuilder buffer = new StringBuilder(); ++ while (matcher.find()) { ++ String s = matcher.group().replace(String.valueOf(LEGACY_CHAR), "").replace('x', '#'); ++ int hex = Integer.decode(s); ++ int red = (hex >> 16) & 0xFF; ++ int green = (hex >> 8) & 0xFF; ++ int blue = hex & 0xFF; ++ String replacement = String.format(RGB_ANSI, red, green, blue); ++ matcher.appendReplacement(buffer, replacement); ++ } ++ matcher.appendTail(buffer); ++ return RGB_PATTERN.matcher(buffer.toString()).replaceAll(result -> { ++ // Purpur end + final int hex = Integer.decode(result.group().substring(1)); + return formatHexAnsi(hex); + }); +@@ -152,10 +169,11 @@ public final class HexFormattingConverter extends LogEventPatternConverter { + } + + private static String stripRGBColors(final String input) { +- return RGB_PATTERN.matcher(input).replaceAll(""); ++ return LEGACY_RGB_PATTERN.matcher(RGB_PATTERN.matcher(input).replaceAll("")).replaceAll(""); // Purpur + } + + static void format(String content, StringBuilder result, int start, boolean ansi) { ++ content = LEGACY_PATTERN.matcher(content).replaceAll(COLOR_CHAR + "$1"); // Purpur + int next = content.indexOf(COLOR_CHAR); + int last = content.length() - 1; + if (next == -1 || next == last) { +diff --git a/src/main/java/io/papermc/paper/logging/SysoutCatcher.java b/src/main/java/io/papermc/paper/logging/SysoutCatcher.java +index a8e813ca89b033f061e695288b3383bdcf128531..1ab65af9359d19530bba7f985a604d2a430ee234 100644 +--- a/src/main/java/io/papermc/paper/logging/SysoutCatcher.java ++++ b/src/main/java/io/papermc/paper/logging/SysoutCatcher.java +@@ -54,9 +54,9 @@ public final class SysoutCatcher { + final JavaPlugin plugin = JavaPlugin.getProvidingPlugin(clazz); + + // Instead of just printing the message, send it to the plugin's logger +- plugin.getLogger().log(this.level, this.prefix + line); ++ plugin.getLogger().log(this.level, /*this.prefix +*/ line); // Purpur - prefix not needed + +- if (SysoutCatcher.SUPPRESS_NAGS) { ++ if (true || SysoutCatcher.SUPPRESS_NAGS) { // Purpur - nagging is annoying + return; + } + if (SysoutCatcher.NAG_INTERVAL > 0 || SysoutCatcher.NAG_TIMEOUT > 0) { +diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +index 89bf48fd581ee6580b91e2eb31dd532cb622df5e..e35da199be67e04c34df6bc09afd8d8122cb0487 100644 +--- a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java ++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +@@ -102,6 +102,7 @@ public class PluginInitializerManager { + java.util.List files = (java.util.List) optionSet.valuesOf("add-plugin"); + // Register plugins from the flag + io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files); ++ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.SparkProviderSource.INSTANCE, new File("cache", "spark.jar").toPath()); // Purpur + } + + // This will be the end of me... +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a7d1ae53eac94bc2dcf8bc78ef1da0d3b8554736 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/SparkProviderSource.java +@@ -0,0 +1,102 @@ ++package io.papermc.paper.plugin.provider.source; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.EntrypointHandler; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.provider.PluginProvider; ++import java.io.BufferedReader; ++import java.io.File; ++import java.io.InputStreamReader; ++import java.math.BigInteger; ++import java.net.URL; ++import java.net.URLConnection; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.security.MessageDigest; ++import java.util.stream.Collectors; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.slf4j.Logger; ++ ++public class SparkProviderSource extends FileProviderSource { ++ public static final SparkProviderSource INSTANCE = new SparkProviderSource(); ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public SparkProviderSource() { ++ super("File '%s' specified by Purpur"::formatted); ++ } ++ ++ @Override ++ public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception { ++ // first, check if user doesn't want spark at all ++ if (Boolean.getBoolean("Purpur.IReallyDontWantSpark")) { ++ return; // boo! ++ } ++ ++ // second, check if user has their own spark ++ if (hasSpark()) { ++ LOGGER.info("Purpur: Using user-provided spark plugin instead of our own."); ++ return; // let's hope it's at least the modern version :3 ++ } ++ ++ // you can't have errors in your code if you wrap the entire codebase in a try/catch block ++ try { ++ ++ // make sure the directory exists where we want to keep spark ++ File file = context.toFile(); ++ file.getParentFile().mkdirs(); ++ ++ boolean shouldDownload; ++ ++ // check if our spark exists ++ if (!file.exists()) { ++ // it does not, so let's download it ++ shouldDownload = true; ++ } else { ++ // we have a spark file, let's see if it's up-to-date by comparing shas ++ String fileSha1 = String.format("%040x", new BigInteger(1, MessageDigest.getInstance("SHA-1").digest(Files.readAllBytes(file.toPath())))); ++ String sparkSha1; ++ ++ // luck has a nifty endpoint containing the sha of the newest version ++ URLConnection urlConnection = new URL("https://sparkapi.lucko.me/download/bukkit/sha1").openConnection(); ++ ++ // set a reasonable timeout to prevent servers without internet from hanging for 60+ seconds on startup ++ urlConnection.setReadTimeout(5000); ++ urlConnection.setConnectTimeout(5000); ++ ++ // read it ++ try (BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()))) { ++ sparkSha1 = reader.lines().collect(Collectors.joining("")); ++ } ++ ++ // compare; we only download a new spark if the shas don't match ++ shouldDownload = !fileSha1.equals(sparkSha1); ++ } ++ ++ // ok, finally we can download spark if we need it ++ if (shouldDownload) { ++ URLConnection urlConnection = new URL("https://sparkapi.lucko.me/download/bukkit").openConnection(); ++ urlConnection.setReadTimeout(5000); ++ urlConnection.setConnectTimeout(5000); ++ Files.copy(urlConnection.getInputStream(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); ++ } ++ ++ // register the spark, newly downloaded or existing ++ super.registerProviders(entrypointHandler, context); ++ ++ } catch (Throwable e) { ++ LOGGER.error("Purpur: Failed to download and install spark plugin", e); ++ } ++ } ++ ++ private static boolean hasSpark() { ++ for (PluginProvider provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { ++ if (provider.getMeta().getName().equalsIgnoreCase("spark")) { ++ return true; ++ } ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index 7b6b51392b123d34382233adcf4c3d4867bdaa32..941f3a0d50329658a9380500ef039d7f10a284e2 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -212,6 +212,21 @@ public class CommandSourceStack implements SharedSuggestionProvider, com.destroy + } + // CraftBukkit end + ++ // Purpur start ++ public boolean testPermission(int i, String bukkitPermission) { ++ if (hasPermission(i, bukkitPermission)) { ++ return true; ++ } ++ String permissionMessage = getLevel().getServer().server.getPermissionMessage(); ++ if (!permissionMessage.isBlank()) { ++ for (String line : permissionMessage.replace("", bukkitPermission).split("\n")) { ++ sendFailure(Component.literal(line)); ++ } ++ } ++ return false; ++ } ++ // Purpur end ++ + public Vec3 getPosition() { + return this.worldPosition; + } +@@ -317,6 +332,30 @@ public class CommandSourceStack implements SharedSuggestionProvider, com.destroy + } + } + ++ // Purpur start ++ public void sendSuccess(@Nullable String message) { ++ sendSuccess(message, false); ++ } ++ ++ public void sendSuccess(@Nullable String message, boolean broadcastToOps) { ++ if (message == null) { ++ return; ++ } ++ sendSuccess(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message), broadcastToOps); ++ } ++ ++ public void sendSuccess(@Nullable net.kyori.adventure.text.Component message) { ++ sendSuccess(message, false); ++ } ++ ++ public void sendSuccess(@Nullable net.kyori.adventure.text.Component message, boolean broadcastToOps) { ++ if (message == null) { ++ return; ++ } ++ sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(message), broadcastToOps); ++ } ++ // Purpur end ++ + public void sendSuccess(Component message, boolean broadcastToOps) { + if (this.source.acceptsSuccess() && !this.silent) { + this.source.sendSystemMessage(message); +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index 87cc7562e4a166d078fe11b7f6980497fc0bd33e..08bed4f01a27162902aa63bb8d35a9159fdcfc4e 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -149,7 +149,7 @@ public class Commands { + DamageCommand.register(this.dispatcher, commandRegistryAccess); + DataCommands.register(this.dispatcher); + DataPackCommand.register(this.dispatcher); +- DebugCommand.register(this.dispatcher); ++ //DebugCommand.register(this.dispatcher); // Purpur + DefaultGameModeCommands.register(this.dispatcher); + DifficultyCommand.register(this.dispatcher); + EffectCommands.register(this.dispatcher, commandRegistryAccess); +@@ -222,6 +222,14 @@ public class Commands { + SetPlayerIdleTimeoutCommand.register(this.dispatcher); + StopCommand.register(this.dispatcher); + WhitelistCommand.register(this.dispatcher); ++ org.purpurmc.purpur.command.CreditsCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.DemoCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.PingCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.UptimeCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.TPSBarCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.CompassCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.RamBarCommand.register(this.dispatcher); // Purpur ++ org.purpurmc.purpur.command.RamCommand.register(this.dispatcher); // Purpur + } + + if (environment.includeIntegrated) { +@@ -309,9 +317,9 @@ public class Commands { + public int performCommand(ParseResults parseresults, String s, String label) { // CraftBukkit + CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseresults.getContext().getSource(); + +- commandlistenerwrapper.getServer().getProfiler().push(() -> { ++ /*commandlistenerwrapper.getServer().getProfiler().push(() -> { // Purpur + return "/" + s; +- }); ++ });*/ // Purpur + + byte b0; + +@@ -394,7 +402,7 @@ public class Commands { + b0 = 0; + } + } finally { +- commandlistenerwrapper.getServer().getProfiler().pop(); ++ //commandlistenerwrapper.getServer().getProfiler().pop(); // Purpur + } + + return b0; +@@ -454,6 +462,7 @@ public class Commands { + private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) { + // Paper end - Async command map building + new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper ++ if (PlayerCommandSendEvent.getHandlerList().getRegisteredListeners().length > 0) { // Purpur - skip all this crap if there's nothing listening + PlayerCommandSendEvent event = new PlayerCommandSendEvent(player.getBukkitEntity(), new LinkedHashSet<>(bukkit)); + event.getPlayer().getServer().getPluginManager().callEvent(event); + +@@ -464,6 +473,7 @@ public class Commands { + } + } + // CraftBukkit end ++ } // Purpur - skip event + player.connection.send(new ClientboundCommandsPacket(rootcommandnode)); + } + +diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +index f25b9330e068c7d9e12cb57a7761cfef9ebaf7bc..7e66aaa960ce7b6dda7c064d4c6856cc4b368b58 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +@@ -200,10 +200,10 @@ public class EntitySelector { + + if (this.playerName != null) { + entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName); +- return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); ++ return entityplayer == null || !canSee(source, entityplayer) ? Collections.emptyList() : Lists.newArrayList(entityplayer); // Purpur + } else if (this.entityUUID != null) { + entityplayer = source.getServer().getPlayerList().getPlayer(this.entityUUID); +- return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); ++ return entityplayer == null || !canSee(source, entityplayer) ? Collections.emptyList() : Lists.newArrayList(entityplayer); // Purpur + } else { + Vec3 vec3d = (Vec3) this.position.apply(source.getPosition()); + Predicate predicate = this.getPredicate(vec3d); +@@ -213,7 +213,7 @@ public class EntitySelector { + ServerPlayer entityplayer1 = (ServerPlayer) source.getEntity(); + + if (predicate.test(entityplayer1)) { +- return Lists.newArrayList(new ServerPlayer[]{entityplayer1}); ++ return !canSee(source, entityplayer1) ? Collections.emptyList() : Lists.newArrayList(entityplayer1); // Purpur + } + } + +@@ -224,6 +224,7 @@ public class EntitySelector { + + if (this.isWorldLimited()) { + object = source.getLevel().getPlayers(predicate, i); ++ ((List) object).removeIf(entityplayer3 -> !canSee(source, (ServerPlayer) entityplayer3)); // Purpur + } else { + object = Lists.newArrayList(); + Iterator iterator = source.getServer().getPlayerList().getPlayers().iterator(); +@@ -231,7 +232,7 @@ public class EntitySelector { + while (iterator.hasNext()) { + ServerPlayer entityplayer2 = (ServerPlayer) iterator.next(); + +- if (predicate.test(entityplayer2)) { ++ if (predicate.test(entityplayer2) && canSee(source, entityplayer2)) { // Purpur + ((List) object).add(entityplayer2); + if (((List) object).size() >= i) { + return (List) object; +@@ -276,4 +277,10 @@ public class EntitySelector { + public static Component joinNames(List entities) { + return ComponentUtils.formatList(entities, Entity::getDisplayName); + } ++ ++ // Purpur start ++ private boolean canSee(CommandSourceStack sender, ServerPlayer target) { ++ return !org.purpurmc.purpur.PurpurConfig.hideHiddenPlayersFromEntitySelector || !(sender.getEntity() instanceof ServerPlayer player) || player.getBukkitEntity().canSee(target.getBukkitEntity()); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index b37e0ff164a894d2033fb94bbbc2f630a0e66bcd..ac335ec4f70830c7687ac4e0aa2a6cba9cb04ae1 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -41,6 +41,12 @@ public class BlockPos extends Vec3i { + private static final int X_OFFSET = 38; + // Paper end + ++ // Purpur start ++ public BlockPos(net.minecraft.world.entity.Entity entity) { ++ super(entity.getBlockX(), entity.getBlockY(), entity.getBlockZ()); ++ } ++ // Purpur end ++ + public BlockPos(int x, int y, int z) { + super(x, y, z); + } +diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java +index 82bce6109d59cba30178a446f0ff129da6f3692f..eaa620ad86abfb151b43f697973cbc731e2e5e92 100644 +--- a/src/main/java/net/minecraft/core/Direction.java ++++ b/src/main/java/net/minecraft/core/Direction.java +@@ -248,6 +248,12 @@ public enum Direction implements StringRepresentable { + case EAST: + var10000 = SOUTH; + break; ++ // Purpur start ++ case UP: ++ return UP; ++ case DOWN: ++ return DOWN; ++ // Purpur end + default: + throw new IllegalStateException("Unable to get Y-rotated facing of " + this); + } +@@ -360,6 +366,12 @@ public enum Direction implements StringRepresentable { + case EAST: + var10000 = NORTH; + break; ++ // Purpur start ++ case UP: ++ return UP; ++ case DOWN: ++ return DOWN; ++ // Purpur end + default: + throw new IllegalStateException("Unable to get CCW facing of " + this); + } +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 88d18d18d69876c98e199acb647c6cca9448d55d..da9cc93f560269a00f0093ad76aba3a05eedb046 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -52,6 +52,7 @@ import net.minecraft.world.item.SpawnEggItem; + import net.minecraft.world.item.alchemy.PotionUtils; + import net.minecraft.world.item.alchemy.Potions; + import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.AnvilBlock; + import net.minecraft.world.level.block.BaseFireBlock; + import net.minecraft.world.level.block.BeehiveBlock; + import net.minecraft.world.level.block.Block; +@@ -1168,6 +1169,23 @@ public interface DispenseItemBehavior { + } + } + }); ++ // Purpur start ++ DispenserBlock.registerBehavior(Items.ANVIL, (new OptionalDispenseItemBehavior() { ++ @Override ++ public ItemStack execute(BlockSource dispenser, ItemStack stack) { ++ Level level = dispenser.getLevel(); ++ if (!level.purpurConfig.dispenserPlaceAnvils) return super.execute(dispenser, stack); ++ Direction facing = dispenser.getBlockState().getValue(DispenserBlock.FACING); ++ BlockPos pos = dispenser.getPos().relative(facing); ++ BlockState state = level.getBlockState(pos); ++ if (state.isAir()) { ++ level.setBlockAndUpdate(pos, Blocks.ANVIL.defaultBlockState().setValue(AnvilBlock.FACING, facing.getAxis() == Direction.Axis.Y ? Direction.NORTH : facing.getClockWise())); ++ stack.shrink(1); ++ } ++ return stack; ++ } ++ })); ++ // Purpur end + } + + static void setEntityPokingOutOfBlock(BlockSource pointer, Entity entity, Direction direction) { +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index d1127d93a85a837933d0d73c24cacac4adc3a5b9..d9a6d273108165f59b995b1fd7748cb5c12b8b1f 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -107,7 +107,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + continue; + } + // CraftBukkit end +- ishearable.shear(SoundSource.BLOCKS); ++ ishearable.shear(SoundSource.BLOCKS, net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.MOB_LOOTING, CraftItemStack.asNMSCopy(craftItem))); // Purpur + worldserver.gameEvent((Entity) null, GameEvent.SHEAR, blockposition); + return true; + } +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index f9e10bf048929886db3c414038d2c7e9f84226a6..323416311f14f5ad887f05183ad3b4921981aecd 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -572,11 +572,20 @@ public class Connection extends SimpleChannelInboundHandler> { + private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper + private static int joinAttemptsThisTick; // Paper + private static int currTick; // Paper ++ private static int tickSecond; // Purpur + public void tick() { + this.flushQueue(); + // Paper start + if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { + Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.maxJoinsPerSecond) { ++ if (++Connection.tickSecond > 20) { ++ Connection.tickSecond = 0; ++ Connection.joinAttemptsThisTick = 0; ++ } ++ } else ++ // Purpur end + Connection.joinAttemptsThisTick = 0; + } + // Paper end +diff --git a/src/main/java/net/minecraft/network/FriendlyByteBuf.java b/src/main/java/net/minecraft/network/FriendlyByteBuf.java +index 9938bb90bef84cf784f9a1ceb02a1a45aa8b48a1..1f4b64a5f812376c499c98cb4be62469bd0b7dbe 100644 +--- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java ++++ b/src/main/java/net/minecraft/network/FriendlyByteBuf.java +@@ -98,6 +98,8 @@ public class FriendlyByteBuf extends ByteBuf { + private static final int MAX_PUBLIC_KEY_LENGTH = 512; + private static final Gson GSON = new Gson(); + ++ public static boolean hasItemSerializeEvent = false; // Purpur ++ + public FriendlyByteBuf(ByteBuf parent) { + this.source = parent; + } +@@ -679,6 +681,17 @@ public class FriendlyByteBuf extends ByteBuf { + this.writeBoolean(false); + } else { + this.writeBoolean(true); ++ // Purpur start ++ if (hasItemSerializeEvent) { ++ var event = new org.purpurmc.purpur.event.packet.NetworkItemSerializeEvent(stack.asBukkitCopy()); ++ event.callEvent(); ++ ItemStack newStack = ItemStack.fromBukkitCopy(event.getItemStack()); ++ if (org.purpurmc.purpur.PurpurConfig.fixNetworkSerializedItemsInCreative && !ItemStack.matches(stack, newStack)) { ++ stack.save(newStack.getOrCreateTagElement("Purpur.OriginalItem")); ++ } ++ stack = newStack; ++ } ++ // Purpur end + Item item = stack.getItem(); + + this.writeId(BuiltInRegistries.ITEM, item); +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index d2f0a0755317f5fa9a1ccf7db346aa77fd287d80..03852e7d21d9470a4469676367463fefb38acdc6 100644 +--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java ++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +@@ -47,7 +47,7 @@ public class PacketUtils { + if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590 + if (listener.isAcceptingMessages()) { + co.aikar.timings.Timing timing = co.aikar.timings.MinecraftTimings.getPacketTiming(packet); // Paper - timings +- try (co.aikar.timings.Timing ignored = timing.startTiming()) { // Paper - timings ++ try { // Paper - timings // Purpur + packet.handle(listener); + } catch (Exception exception) { + if (listener.shouldPropagateHandlingExceptions()) { +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java +index 53b75f5737a910ffc5448cd9a85eae57f9c1488f..ea95873dd034779e56a8b924cd27f9375be05daf 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundPlayerCombatKillPacket.java +@@ -9,6 +9,7 @@ public class ClientboundPlayerCombatKillPacket implements Packet { + private final long gameTime; +- private final long dayTime; ++ private long dayTime; public void setDayTime(long dayTime) { this.dayTime = dayTime; } // Purpur + + public ClientboundSetTimePacket(long time, long timeOfDay, boolean doDaylightCycle) { + this.gameTime = time; +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 0f6d1c56efbab0f9b84f09f7dc27eb705f4006a9..beb05039926e1fb7a656dfcd0c503f82db67fc46 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -250,7 +250,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public Commands vanillaCommandDispatcher; +@@ -305,10 +306,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ //this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; // Purpur ++ //this.profiler = this.metricsRecorder.getProfiler(); // Purpur ++ /*this.onMetricsRecordingStopped = (methodprofilerresults) -> { // Purpur + this.stopRecordingMetrics(); +- }; +- this.onMetricsRecordingFinished = (path) -> { +- }; ++ };*/ // Purpur ++ //this.onMetricsRecordingFinished = (path) -> { // Purpur ++ //}; // Purpur + this.random = RandomSource.create(); + this.port = -1; + this.levels = Maps.newLinkedHashMap(); +@@ -937,13 +940,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + return !this.canOversleep(); + }); +- isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); ++ //isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); // Purpur + // Paper end + new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper + +@@ -1407,7 +1432,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.tickCount % autosavePeriod == 0; + try { + this.isSaving = true; +@@ -1422,20 +1447,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Purpur + Iterator iterator = this.getAllLevels().iterator(); // Paper - move down + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); + worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper + net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper + worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ worldserver.hasRidableMoveEvent = org.purpurmc.purpur.event.entity.RidableMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Purpur + +- this.profiler.push(() -> { ++ /*this.profiler.push(() -> { // Purpur + return worldserver + " " + worldserver.dimension().location(); +- }); ++ });*/ // Purpur + /* Drop global time updates + if (this.tickCount % 20 == 0) { +- this.profiler.push("timeSync"); ++ //this.profiler.push("timeSync"); // Purpur + this.synchronizeTime(worldserver); +- this.profiler.pop(); ++ //this.profiler.pop(); // Purpur + } + // CraftBukkit end */ + +- this.profiler.push("tick"); ++ //this.profiler.push("tick"); // Purpur + + try { +- worldserver.timings.doTick.startTiming(); // Spigot ++ //worldserver.timings.doTick.startTiming(); // Spigot // Purpur + worldserver.tick(shouldKeepTicking); + // Paper start + for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) { + regionManager.recalculateRegions(); + } + // Paper end +- worldserver.timings.doTick.stopTiming(); // Spigot ++ //worldserver.timings.doTick.stopTiming(); // Spigot // Purpur + } catch (Throwable throwable) { + // Spigot Start + CrashReport crashreport; +@@ -1567,33 +1594,33 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Pufferfish - Pufferfish > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ return org.purpurmc.purpur.PurpurConfig.serverModName; // Purpur - Purpur > // Pufferfish - Pufferfish > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! + } + + public SystemReport fillSystemReport(SystemReport details) { +@@ -1877,17 +1904,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + this.executeBlocking(() -> { + this.saveDebugReport(path.resolve("server")); +@@ -2519,40 +2541,40 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop resultConsumer, Consumer dumpConsumer) { +- this.onMetricsRecordingStopped = (methodprofilerresults) -> { ++ /*this.onMetricsRecordingStopped = (methodprofilerresults) -> { // Purpur + this.stopRecordingMetrics(); + resultConsumer.accept(methodprofilerresults); + }; + this.onMetricsRecordingFinished = dumpConsumer; +- this.willStartRecordingMetrics = true; ++ this.willStartRecordingMetrics = true;*/ // Purpur + } + + public void stopRecordingMetrics() { +- this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; ++ //this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; // Purpur + } + + public void finishRecordingMetrics() { +- this.metricsRecorder.end(); ++ //this.metricsRecorder.end(); // Purpur + } + + public void cancelRecordingMetrics() { +- this.metricsRecorder.cancel(); +- this.profiler = this.metricsRecorder.getProfiler(); ++ //this.metricsRecorder.cancel(); // Purpur ++ //this.profiler = this.metricsRecorder.getProfiler(); // Purpur + } + + public Path getWorldPath(LevelResource worldSavePath) { +@@ -2601,15 +2623,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + final io.papermc.paper.adventure.ChatDecorationProcessor processor = new io.papermc.paper.adventure.ChatDecorationProcessor(this, sender, commandSourceStack, message); +@@ -2775,7 +2806,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop functions, ResourceLocation label) { +- ProfilerFiller gameprofilerfiller = this.server.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.server.getProfiler(); // Purpur + + Objects.requireNonNull(label); +- gameprofilerfiller.push(label::toString); ++ //gameprofilerfiller.push(label::toString); // Purpur + Iterator iterator = functions.iterator(); + + while (iterator.hasNext()) { +@@ -69,7 +69,7 @@ public class ServerFunctionManager { + this.execute(customfunction, this.getGameLoopSender()); + } + +- this.server.getProfiler().pop(); ++ //this.server.getProfiler().pop(); // Purpur + } + + public int execute(CommandFunction function, CommandSourceStack source) { +@@ -88,7 +88,7 @@ public class ServerFunctionManager { + } else { + int i; + +- try (co.aikar.timings.Timing timing = function.getTiming().startTiming()) { // Paper ++ try /*(co.aikar.timings.Timing timing = function.getTiming().startTiming())*/ { // Paper // Purpur + this.context = new ServerFunctionManager.ExecutionContext(tracer); + i = this.context.runTopCommand(function, source); + } finally { +@@ -177,10 +177,10 @@ public class ServerFunctionManager { + + try { + ServerFunctionManager.QueuedCommand customfunctiondata_b = (ServerFunctionManager.QueuedCommand) this.commandQueue.removeFirst(); +- ProfilerFiller gameprofilerfiller = ServerFunctionManager.this.server.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = ServerFunctionManager.this.server.getProfiler(); // Purpur + + Objects.requireNonNull(customfunctiondata_b); +- gameprofilerfiller.push(customfunctiondata_b::toString); ++ //gameprofilerfiller.push(customfunctiondata_b::toString); // Purpur + this.depth = customfunctiondata_b.depth; + customfunctiondata_b.execute(ServerFunctionManager.this, this.commandQueue, i, this.tracer); + if (!this.nestedCalls.isEmpty()) { +@@ -192,7 +192,7 @@ public class ServerFunctionManager { + this.nestedCalls.clear(); + } + } finally { +- ServerFunctionManager.this.server.getProfiler().pop(); ++ //ServerFunctionManager.this.server.getProfiler().pop(); // Purpur + } + + ++j; +diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +index e639c0ec642910e66b1d68ae0b9208ef58d91fce..24c4ad919eeb9c5e15572ee32b0895c993ac6735 100644 +--- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java ++++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +@@ -48,7 +48,7 @@ public class EnchantCommand { + + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { + Enchantment enchantment2 = enchantment.value(); +- if (level > enchantment2.getMaxLevel()) { ++ if (!org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchantCommand && level > enchantment2.getMaxLevel()) { // Purpur + throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment2.getMaxLevel()); + } else { + int i = 0; +@@ -58,7 +58,7 @@ public class EnchantCommand { + LivingEntity livingEntity = (LivingEntity)entity; + ItemStack itemStack = livingEntity.getMainHandItem(); + if (!itemStack.isEmpty()) { +- if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) { ++ if ((enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchantCommand && !itemStack.hasEnchantment(enchantment2))) { // Purpur + itemStack.enchant(enchantment2, level); + ++i; + } else if (targets.size() == 1) { +diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +index 27c0aaf123c3e945eb24e8a3892bd8ac42115733..85e1c1d6eb4472baa958b4f482791e8479dfcbf0 100644 +--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +@@ -41,6 +41,18 @@ public class GameModeCommand { + } + + private static int setMode(CommandContext context, Collection targets, GameType gameMode) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.commandGamemodeRequiresPermission) { ++ String gamemode = gameMode.getName(); ++ CommandSourceStack sender = context.getSource(); ++ if (!sender.testPermission(2, "minecraft.command.gamemode." + gamemode)) { ++ return 0; ++ } ++ if (sender.getEntity() instanceof ServerPlayer player && (targets.size() > 1 || !targets.contains(player)) && !sender.testPermission(2, "minecraft.command.gamemode." + gamemode + ".other")) { ++ return 0; ++ } ++ } ++ // Purpur end + int i = 0; + + for(ServerPlayer serverPlayer : targets) { +diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java +index ee7d29d85c8b024c9b23cba8ecd4192aa7e8aa7b..7a44bac6e66bc5f5fe14a45a5e7c78c940fb1efb 100644 +--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java +@@ -58,6 +58,7 @@ public class GiveCommand { + boolean flag = entityplayer.getInventory().add(itemstack); + ItemEntity entityitem; + ++ if (org.purpurmc.purpur.PurpurConfig.disableGiveCommandDrops) continue; // Purpur - add config option for toggling give command dropping + if (flag && itemstack.isEmpty()) { + itemstack.setCount(1); + entityitem = entityplayer.drop(itemstack, false, false, false); // SPIGOT-2942: Add boolean to call event +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index ad4fdbdcf09f30d10e61ccf47f8fb9ce6bd92e73..6ecc75621867390738e804e06ac284524664473d 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -99,6 +99,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + return; + } + // Paper start - Use TerminalConsoleAppender ++ if (DedicatedServer.this.gui == null || System.console() != null) // Purpur - has no GUI or has console (did not double-click) + new com.destroystokyo.paper.console.PaperConsole(DedicatedServer.this).start(); + /* + jline.console.ConsoleReader bufferedreader = reader; +@@ -218,9 +219,19 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + thread.start(); // Paper - start console thread after MinecraftServer.console & PaperConfig are initialized + io.papermc.paper.command.PaperCommands.registerCommands(this); + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); ++ // Purpur start ++ try { ++ org.purpurmc.purpur.PurpurConfig.init((java.io.File) options.valueOf("purpur-settings")); ++ } catch (Exception e) { ++ DedicatedServer.LOGGER.error("Unable to load server configuration", e); ++ return false; ++ } ++ org.purpurmc.purpur.PurpurConfig.registerCommands(); ++ // Purpur end + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // load version history now + io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider + // Paper end ++ gg.pufferfish.pufferfish.PufferfishConfig.pufferfishFile = (java.io.File) options.valueOf("pufferfish-settings"); // Purpur + gg.pufferfish.pufferfish.PufferfishConfig.load(); // Pufferfish + gg.pufferfish.pufferfish.PufferfishCommand.init(); // Pufferfish + +@@ -269,6 +280,30 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + DedicatedServer.LOGGER.warn("Perhaps a server is already running on that port?"); + return false; + } ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useUPnP) { ++ LOGGER.info("[UPnP] Attempting to start UPnP port forwarding service..."); ++ if (dev.omega24.upnp4j.UPnP4J.isUPnPAvailable()) { ++ if (dev.omega24.upnp4j.UPnP4J.isOpen(this.getPort(), dev.omega24.upnp4j.util.Protocol.TCP)) { ++ this.upnp = false; ++ LOGGER.info("[UPnP] Port {} is already open", this.getPort()); ++ } else if (dev.omega24.upnp4j.UPnP4J.open(this.getPort(), dev.omega24.upnp4j.util.Protocol.TCP)) { ++ this.upnp = true; ++ LOGGER.info("[UPnP] Successfully opened port {}", this.getPort()); ++ } else { ++ this.upnp = false; ++ LOGGER.info("[UPnP] Failed to open port {}", this.getPort()); ++ } ++ ++ if (upnp) { ++ LOGGER.info("[UPnP] {}:{}", dev.omega24.upnp4j.UPnP4J.getExternalIP(), this.getPort()); ++ } ++ } else { ++ this.upnp = false; ++ LOGGER.error("[UPnP] Service is unavailable"); ++ } ++ } ++ // Purpur end + + // CraftBukkit start + // this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage)); // Spigot - moved up +@@ -342,6 +377,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + if (gg.pufferfish.pufferfish.PufferfishConfig.enableAsyncMobSpawning) mobSpawnExecutor.start(); // Pufferfish ++ org.purpurmc.purpur.task.BossBarTask.startAll(); // Purpur ++ org.purpurmc.purpur.task.BeehiveTask.instance().register(); // Purpur ++ + return true; + } + } +@@ -488,7 +526,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + public void handleConsoleInputs() { +- MinecraftTimings.serverCommandTimer.startTiming(); // Spigot ++ //MinecraftTimings.serverCommandTimer.startTiming(); // Spigot // Purpur + // Paper start - use proper queue + ConsoleInput servercommand; + while ((servercommand = this.serverCommandQueue.poll()) != null) { +@@ -505,7 +543,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + // CraftBukkit end + } + +- MinecraftTimings.serverCommandTimer.stopTiming(); // Spigot ++ //MinecraftTimings.serverCommandTimer.stopTiming(); // Spigot // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +index 818289e831e3dad29345c43265e2efd7689bc500..1ea3012995c738c67b31e997c138f824f9e69ba1 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +@@ -58,6 +58,7 @@ public class DedicatedServerProperties extends Settings finalizers = Lists.newArrayList(); + final AtomicBoolean isClosing = new AtomicBoolean(); ++ // Purpur start ++ private final CommandHistory history = new CommandHistory(); ++ private String currentCommand = ""; ++ private int historyIndex = 0; ++ // Purpur end + + public static MinecraftServerGui showFrameFor(final DedicatedServer server) { + try { +@@ -51,7 +56,7 @@ public class MinecraftServerGui extends JComponent { + ; + } + +- final JFrame jframe = new JFrame("Minecraft server"); ++ final JFrame jframe = new JFrame("Purpur Minecraft server"); // Purpur + final MinecraftServerGui servergui = new MinecraftServerGui(server); + + jframe.setDefaultCloseOperation(2); +@@ -59,7 +64,7 @@ public class MinecraftServerGui extends JComponent { + jframe.pack(); + jframe.setLocationRelativeTo((Component) null); + jframe.setVisible(true); +- jframe.setName("Minecraft server"); // Paper ++ jframe.setName("Purpur Minecraft server"); // Paper // Purpur + + // Paper start - Add logo as frame image + try { +@@ -71,7 +76,7 @@ public class MinecraftServerGui extends JComponent { + jframe.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent windowevent) { + if (!servergui.isClosing.getAndSet(true)) { +- jframe.setTitle("Minecraft server - shutting down!"); ++ jframe.setTitle("Purpur Minecraft server - shutting down!"); // Purpur + server.halt(true); + servergui.runFinalizers(); + } +@@ -125,7 +130,7 @@ public class MinecraftServerGui extends JComponent { + + private JComponent buildChatPanel() { + JPanel jpanel = new JPanel(new BorderLayout()); +- JTextArea jtextarea = new JTextArea(); ++ org.purpurmc.purpur.gui.JColorTextPane jtextarea = new org.purpurmc.purpur.gui.JColorTextPane(); // Purpur + JScrollPane jscrollpane = new JScrollPane(jtextarea, 22, 30); + + jtextarea.setEditable(false); +@@ -137,10 +142,43 @@ public class MinecraftServerGui extends JComponent { + + if (!s.isEmpty()) { + this.server.handleConsoleInput(s, this.server.createCommandSourceStack()); ++ // Purpur start ++ history.add(s); ++ historyIndex = -1; ++ // Purpur end + } + + jtextfield.setText(""); + }); ++ // Purpur start ++ jtextfield.getInputMap().put(javax.swing.KeyStroke.getKeyStroke("UP"), "up"); ++ jtextfield.getInputMap().put(javax.swing.KeyStroke.getKeyStroke("DOWN"), "down"); ++ jtextfield.getActionMap().put("up", new javax.swing.AbstractAction() { ++ @Override ++ public void actionPerformed(java.awt.event.ActionEvent actionEvent) { ++ if (historyIndex < 0) { ++ currentCommand = jtextfield.getText(); ++ } ++ if (historyIndex < history.size() - 1) { ++ jtextfield.setText(history.get(historyIndex)); ++ } ++ } ++ }); ++ jtextfield.getActionMap().put("down", new javax.swing.AbstractAction() { ++ @Override ++ public void actionPerformed(java.awt.event.ActionEvent actionEvent) { ++ if (historyIndex >= 0) { ++ if (historyIndex == 0) { ++ --historyIndex; ++ jtextfield.setText(currentCommand); ++ } else { ++ --historyIndex; ++ jtextfield.setText(history.get(historyIndex)); ++ } ++ } ++ } ++ }); ++ // Purpur end + jtextarea.addFocusListener(new FocusAdapter() { + public void focusGained(FocusEvent focusevent) {} + }); +@@ -176,7 +214,7 @@ public class MinecraftServerGui extends JComponent { + } + + private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]"); // CraftBukkit +- public void print(JTextArea textArea, JScrollPane scrollPane, String message) { ++ public void print(org.purpurmc.purpur.gui.JColorTextPane textArea, JScrollPane scrollPane, String message) { // Purpur + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(() -> { + this.print(textArea, scrollPane, message); +@@ -190,11 +228,14 @@ public class MinecraftServerGui extends JComponent { + flag = (double) jscrollbar.getValue() + jscrollbar.getSize().getHeight() + (double) (MinecraftServerGui.MONOSPACED.getSize() * 4) > (double) jscrollbar.getMaximum(); + } + ++ /* // Purpur + try { + document.insertString(document.getLength(), MinecraftServerGui.ANSI.matcher(message).replaceAll(""), (AttributeSet) null); // CraftBukkit + } catch (BadLocationException badlocationexception) { + ; + } ++ */ // Purpur ++ textArea.append(message); // Purpur + + if (flag) { + jscrollbar.setValue(Integer.MAX_VALUE); +@@ -202,4 +243,16 @@ public class MinecraftServerGui extends JComponent { + + } + } ++ ++ // Purpur start ++ public static class CommandHistory extends java.util.LinkedList { ++ @Override ++ public boolean add(String command) { ++ if (size() > 1000) { ++ remove(); ++ } ++ return super.offerFirst(command); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 3ce4dbf4eed442d89d6bbc8e4c6a000172041da5..57fdef8b16e1ed9a4693356144b4685bbcea285c 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -621,20 +621,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + protected void tick(BooleanSupplier shouldKeepTicking) { +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper +- gameprofilerfiller.push("poi"); ++ //try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper // Purpur ++ //gameprofilerfiller.push("poi"); // Purpur + this.poiManager.tick(shouldKeepTicking); +- } // Paper +- gameprofilerfiller.popPush("chunk_unload"); ++ //} // Paper // Purpur ++ //gameprofilerfiller.popPush("chunk_unload"); // Purpur + if (!this.level.noSave()) { +- try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper ++ //try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper // Purpur + this.processUnloads(shouldKeepTicking); +- } // Paper ++ //} // Paper // Purpur + } + +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + + public boolean hasWork() { +@@ -998,7 +998,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + return this.anyPlayerCloseEnoughForSpawning(this.getUpdatingChunkIfPresent(chunkcoordintpair.toLong()), chunkcoordintpair, reducedRange); + } + +- final boolean anyPlayerCloseEnoughForSpawning(ChunkHolder playerchunk, ChunkPos chunkcoordintpair, boolean reducedRange) { ++ public final boolean anyPlayerCloseEnoughForSpawning(ChunkHolder playerchunk, ChunkPos chunkcoordintpair, boolean reducedRange) { // Purpur - package -> public + // this function is so hot that removing the map lookup call can have an order of magnitude impact on its performance + // tested and confirmed via System.nanoTime() + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange; +@@ -1253,24 +1253,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - optimised tracker + private final void processTrackQueue() { +- this.level.timings.tracker1.startTiming(); ++ //this.level.timings.tracker1.startTiming(); // Purpur + try { + for (TrackedEntity tracker : this.entityMap.values()) { + // update tracker entry + tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); + } + } finally { +- this.level.timings.tracker1.stopTiming(); ++ //this.level.timings.tracker1.stopTiming(); // Purpur + } + + +- this.level.timings.tracker2.startTiming(); ++ //this.level.timings.tracker2.startTiming(); // Purpur + try { + for (TrackedEntity tracker : this.entityMap.values()) { + tracker.serverEntity.sendChanges(); + } + } finally { +- this.level.timings.tracker2.stopTiming(); ++ //this.level.timings.tracker2.stopTiming(); // Purpur + } + } + // Paper end - optimised tracker +@@ -1285,7 +1285,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + List list = Lists.newArrayList(); + List list1 = this.level.players(); + ObjectIterator objectiterator = this.entityMap.values().iterator(); +- level.timings.tracker1.startTiming(); // Paper ++ //level.timings.tracker1.startTiming(); // Paper // Purpur + + ChunkMap.TrackedEntity playerchunkmap_entitytracker; + +@@ -1310,17 +1310,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + playerchunkmap_entitytracker.serverEntity.sendChanges(); + } + } +- level.timings.tracker1.stopTiming(); // Paper ++ //level.timings.tracker1.stopTiming(); // Paper // Purpur + + if (!list.isEmpty()) { + objectiterator = this.entityMap.values().iterator(); + +- level.timings.tracker2.startTiming(); // Paper ++ //level.timings.tracker2.startTiming(); // Paper // Purpur + while (objectiterator.hasNext()) { + playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); + playerchunkmap_entitytracker.updatePlayers(list); + } +- level.timings.tracker2.stopTiming(); // Paper ++ //level.timings.tracker2.stopTiming(); // Paper // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index c6f5d6756fa0e068a462d9c0ded12e0771abba37..0ae45cf5a084fd412305e8b2f5dabe608b4eb1c1 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -431,16 +431,16 @@ public class ServerChunkCache extends ChunkSource { + return ifLoaded; + } + // Paper end +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- gameprofilerfiller.incrementCounter("getChunk"); ++ //gameprofilerfiller.incrementCounter("getChunk"); // Purpur + long k = ChunkPos.asLong(x, z); + + ChunkAccess ichunkaccess; + + // Paper - rewrite chunk system - there are no correct callbacks to remove items from cache in the new chunk system + +- gameprofilerfiller.incrementCounter("getChunkCacheMiss"); ++ //gameprofilerfiller.incrementCounter("getChunkCacheMiss"); // Purpur + CompletableFuture> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create, true); // Paper + ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor; + +@@ -450,10 +450,10 @@ public class ServerChunkCache extends ChunkSource { + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system + // Paper end + com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x1, z1); // Paper - sync load info +- this.level.timings.syncChunkLoad.startTiming(); // Paper ++ //this.level.timings.syncChunkLoad.startTiming(); // Paper // Purpur + chunkproviderserver_b.managedBlock(completablefuture::isDone); + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - async chunk debug // Paper - rewrite chunk system +- this.level.timings.syncChunkLoad.stopTiming(); // Paper ++ //this.level.timings.syncChunkLoad.stopTiming(); // Paper // Purpur + } // Paper + ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> { + return ichunkaccess1; +@@ -601,17 +601,17 @@ public class ServerChunkCache extends ChunkSource { + + public void save(boolean flush) { + this.runDistanceManagerUpdates(); +- try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ //try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings // Purpur + this.chunkMap.saveAllChunks(flush); +- } // Paper - Timings ++ //} // Paper - Timings // Purpur + } + + // Paper start - duplicate save, but call incremental + public void saveIncrementally() { + this.runDistanceManagerUpdates(); +- try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ //try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings // Purpur + this.chunkMap.saveIncrementally(); +- } // Paper - Timings ++ //} // Paper - Timings // Purpur + } + // Paper end + +@@ -628,36 +628,36 @@ public class ServerChunkCache extends ChunkSource { + // CraftBukkit start - modelled on below + public void purgeUnload() { + if (true) return; // Paper - tickets will be removed later, this behavior isn't really well accounted for by the chunk system +- this.level.getProfiler().push("purge"); ++ //this.level.getProfiler().push("purge"); // Purpur + this.distanceManager.purgeStaleTickets(); + this.runDistanceManagerUpdates(); +- this.level.getProfiler().popPush("unload"); ++ //this.level.getProfiler().popPush("unload"); // Purpur + this.chunkMap.tick(() -> true); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + this.clearCache(); + } + // CraftBukkit end + + @Override + public void tick(BooleanSupplier shouldKeepTicking, boolean tickChunks) { +- this.level.getProfiler().push("purge"); +- this.level.timings.doChunkMap.startTiming(); // Spigot ++ //this.level.getProfiler().push("purge"); // Purpur ++ //this.level.timings.doChunkMap.startTiming(); // Spigot // Purpur + this.distanceManager.purgeStaleTickets(); + this.runDistanceManagerUpdates(); +- this.level.timings.doChunkMap.stopTiming(); // Spigot +- this.level.getProfiler().popPush("chunks"); ++ //this.level.timings.doChunkMap.stopTiming(); // Spigot // Purpur ++ //this.level.getProfiler().popPush("chunks"); // Purpur + if (tickChunks) { +- this.level.timings.chunks.startTiming(); // Paper - timings ++ //this.level.timings.chunks.startTiming(); // Paper - timings // Purpur + this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes + this.tickChunks(); +- this.level.timings.chunks.stopTiming(); // Paper - timings ++ //this.level.timings.chunks.stopTiming(); // Paper - timings // Purpur + } + +- this.level.timings.doChunkUnload.startTiming(); // Spigot +- this.level.getProfiler().popPush("unload"); ++ //this.level.timings.doChunkUnload.startTiming(); // Spigot // Purpur ++ //this.level.getProfiler().popPush("unload"); // Purpur + this.chunkMap.tick(shouldKeepTicking); +- this.level.timings.doChunkUnload.stopTiming(); // Spigot +- this.level.getProfiler().pop(); ++ //this.level.timings.doChunkUnload.stopTiming(); // Spigot // Purpur ++ //this.level.getProfiler().pop(); // Purpur + this.clearCache(); + } + +@@ -703,15 +703,15 @@ public class ServerChunkCache extends ChunkSource { + } + // Paper end - optimize isOutisdeRange + LevelData worlddata = this.level.getLevelData(); +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.level.getProfiler(); // Purpur + +- gameprofilerfiller.push("pollingChunks"); ++ //gameprofilerfiller.push("pollingChunks"); // Purpur + this.level.resetIceAndSnowTick(); // Pufferfish - reset ice & snow tick random + int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit + +- gameprofilerfiller.push("naturalSpawnCount"); +- this.level.timings.countNaturalMobs.startTiming(); // Paper - timings ++ //gameprofilerfiller.push("naturalSpawnCount"); // Purpur ++ //this.level.timings.countNaturalMobs.startTiming(); // Paper - timings // Purpur + int l = this.distanceManager.getNaturalSpawnChunkCount(); + // Paper start - per player mob spawning + NaturalSpawner.SpawnState spawnercreature_d; // moved down +@@ -732,16 +732,16 @@ public class ServerChunkCache extends ChunkSource { + // Pufferfish end + } + // Paper end +- this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings ++ //this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings // Purpur + + //this.lastSpawnState = spawnercreature_d; // Pufferfish - this is managed asynchronously +- gameprofilerfiller.popPush("filteringLoadedChunks"); ++ //gameprofilerfiller.popPush("filteringLoadedChunks"); // Purpur + // Paper - moved down +- this.level.timings.chunkTicks.startTiming(); // Paper ++ //this.level.timings.chunkTicks.startTiming(); // Paper // Purpur + + // Paper - moved down + +- gameprofilerfiller.popPush("spawnAndTick"); ++ //gameprofilerfiller.popPush("spawnAndTick"); // Purpur + boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + + // Paper - only shuffle if per-player mob spawning is disabled +@@ -791,17 +791,17 @@ public class ServerChunkCache extends ChunkSource { + } + } + // Paper end - optimise chunk tick iteration +- this.level.timings.chunkTicks.stopTiming(); // Paper +- gameprofilerfiller.popPush("customSpawners"); ++ //this.level.timings.chunkTicks.stopTiming(); // Paper // Purpur ++ //gameprofilerfiller.popPush("customSpawners"); // Purpur + if (flag2) { +- try (co.aikar.timings.Timing ignored = this.level.timings.miscMobSpawning.startTiming()) { // Paper - timings ++ //try (co.aikar.timings.Timing ignored = this.level.timings.miscMobSpawning.startTiming()) { // Paper - timings // Purpur + this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); +- } // Paper - timings ++ //} // Paper - timings // Purpur + } +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + // Paper start - use set of chunks requiring updates, rather than iterating every single one loaded +- gameprofilerfiller.popPush("broadcast"); +- this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing ++ //gameprofilerfiller.popPush("broadcast"); // Purpur ++ //this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing // Purpur + if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) { + ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone(); + this.chunkMap.needsChangeBroadcasting.clear(); +@@ -813,8 +813,8 @@ public class ServerChunkCache extends ChunkSource { + } + } + } +- this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing +- gameprofilerfiller.pop(); ++ //this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing // Purpur ++ //gameprofilerfiller.pop(); // Purpur + // Paper end - use set of chunks requiring updates, rather than iterating every single one loaded + // Paper start - controlled flush for entity tracker packets + List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); +@@ -1029,7 +1029,7 @@ public class ServerChunkCache extends ChunkSource { + + @Override + protected void doRunTask(Runnable task) { +- ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); ++ //ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); // Purpur + super.doRunTask(task); + } + +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index d5cb594f0b17ec9dc1a19cdb99bba553e70171be..6afee2a744a3498d4a0eee35f77cde444f73d12c 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -72,7 +72,7 @@ public class ServerEntity { + @Nullable + private List> trackedDataValues; + // CraftBukkit start +- final Set trackedPlayers; // Paper - private -> package ++ public final Set trackedPlayers; // Paper - private -> package // Purpur - package -> public + + public ServerEntity(ServerLevel worldserver, Entity entity, int i, boolean flag, Consumer> consumer, Set trackedPlayers) { + this.trackedPlayers = trackedPlayers; +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 3ee5c3c17d450dce54e051dc53c9df44d9b3dc1b..86b8485c0fb1dc5cd79c9e24546dc74459822a48 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -212,6 +212,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + private final StructureManager structureManager; + private final StructureCheck structureCheck; + private final boolean tickTime; ++ private double preciseTime; // Purpur ++ private boolean forceTime; // Purpur + public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick + + // CraftBukkit start +@@ -220,6 +222,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + public boolean hasPhysicsEvent = true; // Paper + public boolean hasEntityMoveEvent = false; // Paper + private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) ++ public boolean hasRidableMoveEvent = false; // Purpur + public static Throwable getAddToWorldStackTrace(Entity entity) { + final Throwable thr = new Throwable(entity + " Added to world at " + new java.util.Date()); + io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(thr); +@@ -542,7 +545,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.dragonParts = new Int2ObjectOpenHashMap(); + this.tickTime = flag1; + this.server = minecraftserver; +- this.customSpawners = list; ++ // Purpur start - enable/disable MobSpawners per world ++ this.customSpawners = Lists.newArrayList(); ++ if (purpurConfig.phantomSpawning) { ++ customSpawners.add(new net.minecraft.world.level.levelgen.PhantomSpawner()); ++ } ++ if (purpurConfig.patrolSpawning) { ++ customSpawners.add(new net.minecraft.world.level.levelgen.PatrolSpawner()); ++ } ++ if (purpurConfig.catSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.npc.CatSpawner()); ++ } ++ if (purpurConfig.villageSiegeSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.ai.village.VillageSiege()); ++ } ++ if (purpurConfig.villagerTraderSpawning) { ++ customSpawners.add(new net.minecraft.world.entity.npc.WanderingTraderSpawner(iworlddataserver)); ++ } ++ // Purpur end + this.serverLevelData = iworlddataserver; + ChunkGenerator chunkgenerator = worlddimension.generator(); + // CraftBukkit start +@@ -605,6 +625,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system + this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system ++ this.preciseTime = this.serverLevelData.getDayTime(); // Purpur + } + + public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) { +@@ -633,17 +654,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + // Paper end - optimise checkDespawn +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + + this.handlingTick = true; +- gameprofilerfiller.push("world border"); ++ //gameprofilerfiller.push("world border"); // Purpur + this.getWorldBorder().tick(); +- gameprofilerfiller.popPush("weather"); ++ //gameprofilerfiller.popPush("weather"); // Purpur + this.advanceWeatherCycle(); + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + long j; + +- if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { ++ if (this.purpurConfig.playersSkipNight && this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { + // CraftBukkit start + j = this.levelData.getDayTime() + 24000L; + TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); +@@ -665,32 +686,32 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.updateSkyBrightness(); + this.tickTime(); +- gameprofilerfiller.popPush("tickPending"); +- timings.scheduledBlocks.startTiming(); // Paper ++ //gameprofilerfiller.popPush("tickPending"); // Purpur ++ //timings.scheduledBlocks.startTiming(); // Paper // Purpur + if (!this.isDebug()) { + j = this.getGameTime(); +- gameprofilerfiller.push("blockTicks"); ++ //gameprofilerfiller.push("blockTicks"); // Purpur + this.blockTicks.tick(j, 65536, this::tickBlock); +- gameprofilerfiller.popPush("fluidTicks"); ++ //gameprofilerfiller.popPush("fluidTicks"); // Purpur + this.fluidTicks.tick(j, 65536, this::tickFluid); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } +- timings.scheduledBlocks.stopTiming(); // Paper ++ //timings.scheduledBlocks.stopTiming(); // Paper // Purpur + +- gameprofilerfiller.popPush("raid"); +- this.timings.raids.startTiming(); // Paper - timings ++ //gameprofilerfiller.popPush("raid"); // Purpur ++ //this.timings.raids.startTiming(); // Paper - timings // Purpur + this.raids.tick(); +- this.timings.raids.stopTiming(); // Paper - timings +- gameprofilerfiller.popPush("chunkSource"); +- this.timings.chunkProviderTick.startTiming(); // Paper - timings ++ //this.timings.raids.stopTiming(); // Paper - timings // Purpur ++ //gameprofilerfiller.popPush("chunkSource"); // Purpur ++ //this.timings.chunkProviderTick.startTiming(); // Paper - timings // Purpur + this.getChunkSource().tick(shouldKeepTicking, true); +- this.timings.chunkProviderTick.stopTiming(); // Paper - timings +- gameprofilerfiller.popPush("blockEvents"); +- timings.doSounds.startTiming(); // Spigot ++ //this.timings.chunkProviderTick.stopTiming(); // Paper - timings // Purpur ++ //gameprofilerfiller.popPush("blockEvents"); // Purpur ++ //timings.doSounds.startTiming(); // Spigot // Purpur + this.runBlockEvents(); +- timings.doSounds.stopTiming(); // Spigot ++ //timings.doSounds.stopTiming(); // Spigot // Purpur + this.handlingTick = false; +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players + + if (flag) { +@@ -698,25 +719,25 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + if (flag || this.emptyTime++ < 300) { +- gameprofilerfiller.push("entities"); +- timings.tickEntities.startTiming(); // Spigot ++ //gameprofilerfiller.push("entities"); // Purpur ++ //timings.tickEntities.startTiming(); // Spigot // Purpur + if (this.dragonFight != null) { +- gameprofilerfiller.push("dragonFight"); ++ //gameprofilerfiller.push("dragonFight"); // Purpur + this.dragonFight.tick(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + + org.spigotmc.ActivationRange.activateEntities(this); // Spigot +- timings.entityTick.startTiming(); // Spigot ++ //timings.entityTick.startTiming(); // Spigot // Purpur + this.entityTickList.forEach((entity) -> { + entity.activatedPriorityReset = false; // Pufferfish - DAB + if (!entity.isRemoved()) { + if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed + entity.discard(); + } else { +- gameprofilerfiller.push("checkDespawn"); ++ //gameprofilerfiller.push("checkDespawn"); // Purpur + entity.checkDespawn(); +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list + Entity entity1 = entity.getVehicle(); + +@@ -728,7 +749,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + entity.stopRiding(); + } + +- gameprofilerfiller.push("tick"); ++ //gameprofilerfiller.push("tick"); // Purpur + // Pufferfish start - copied from this.guardEntityTick + try { + this.tickNonPassenger(entity); // Pufferfish - changed +@@ -743,20 +764,19 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Paper end + } + // Pufferfish end +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } + } + } + }); +- timings.entityTick.stopTiming(); // Spigot +- timings.tickEntities.stopTiming(); // Spigot +- gameprofilerfiller.pop(); ++ //timings.entityTick.stopTiming(); // Spigot // Purpur ++ //timings.tickEntities.stopTiming(); // Spigot // Purpur ++ //gameprofilerfiller.pop(); // Purpur + this.tickBlockEntities(); + } + +- gameprofilerfiller.push("entityManagement"); ++ //gameprofilerfiller.push("entityManagement"); // Purpur + //this.entityManager.tick(); // Paper - rewrite chunk system +- gameprofilerfiller.pop(); + } + + @Override +@@ -774,6 +794,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.serverLevelData.setGameTime(i); + this.serverLevelData.getScheduledEvents().tick(this.server, i); + if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ // Purpur start ++ int incrementTicks = isDay() ? this.purpurConfig.daytimeTicks : this.purpurConfig.nighttimeTicks; ++ if (incrementTicks != 12000) { ++ this.preciseTime += 12000 / (double) incrementTicks; ++ this.setDayTime(this.preciseTime); ++ } else ++ // Purpur end + this.setDayTime(this.levelData.getDayTime() + 1L); + } + +@@ -782,7 +809,21 @@ public class ServerLevel extends Level implements WorldGenLevel { + + public void setDayTime(long timeOfDay) { + this.serverLevelData.setDayTime(timeOfDay); ++ // Purpur start ++ this.preciseTime = timeOfDay; ++ this.forceTime = false; + } ++ public void setDayTime(double i) { ++ this.serverLevelData.setDayTime((long) i); ++ this.forceTime = true; ++ // Purpur end ++ } ++ ++ // Purpur start ++ public boolean isForceTime() { ++ return this.forceTime; ++ } ++ // Purpur end + + public void tickCustomSpawners(boolean spawnMonsters, boolean spawnAnimals) { + Iterator iterator = this.customSpawners.iterator(); +@@ -807,7 +848,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + // Paper start - optimise random block ticking + private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); +- // private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(); // Pufferfish - moved to super ++ private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - moved to super // Purpur - dont break ABI + // Paper end + + private int currentIceAndSnowTick = 0; protected void resetIceAndSnowTick() { this.currentIceAndSnowTick = this.randomTickRandom.nextInt(16); } // Pufferfish +@@ -817,9 +858,9 @@ public class ServerLevel extends Level implements WorldGenLevel { + boolean flag = this.isRaining(); + int j = chunkcoordintpair.getMinBlockX(); + int k = chunkcoordintpair.getMinBlockZ(); +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push("thunder"); ++ //gameprofilerfiller.push("thunder"); // Purpur + final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change + + if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && /*this.random.nextInt(this.spigotConfig.thunderChance) == 0 &&*/ chunk.shouldDoLightning(this.random)) { // Spigot // Paper - disable thunder // Pufferfish - replace random with shouldDoLightning +@@ -829,10 +870,18 @@ public class ServerLevel extends Level implements WorldGenLevel { + boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * this.paperConfig().entities.spawning.skeletonHorseThunderSpawnChance.or(0.01D) && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper + + if (flag1) { +- SkeletonHorse entityhorseskeleton = (SkeletonHorse) EntityType.SKELETON_HORSE.create(this); ++ // Purpur start ++ net.minecraft.world.entity.animal.horse.AbstractHorse entityhorseskeleton; ++ if (purpurConfig.zombieHorseSpawnChance > 0D && random.nextDouble() <= purpurConfig.zombieHorseSpawnChance) { ++ entityhorseskeleton = EntityType.ZOMBIE_HORSE.create(this); ++ } else { ++ entityhorseskeleton = EntityType.SKELETON_HORSE.create(this); ++ if (entityhorseskeleton != null) ((SkeletonHorse) entityhorseskeleton).setTrap(true); ++ } ++ // Purpur end + + if (entityhorseskeleton != null) { +- entityhorseskeleton.setTrap(true); ++ //entityhorseskeleton.setTrap(true); // Purpur - moved up + entityhorseskeleton.setAge(0); + entityhorseskeleton.setPos((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()); + this.addFreshEntity(entityhorseskeleton, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit +@@ -849,7 +898,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + +- gameprofilerfiller.popPush("iceandsnow"); ++ //gameprofilerfiller.popPush("iceandsnow"); // Purpur + int l; + + if (!this.paperConfig().environment.disableIceAndSnow && (this.currentIceAndSnowTick++ & 15) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking // Pufferfish - optimize further random ticking +@@ -900,8 +949,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + // Paper start - optimise random block ticking +- gameprofilerfiller.popPush("randomTick"); +- timings.chunkTicksBlocks.startTiming(); // Paper ++ //gameprofilerfiller.popPush("randomTick"); // Purpur ++ //timings.chunkTicksBlocks.startTiming(); // Paper // Purpur + if (randomTickSpeed > 0) { + LevelChunkSection[] sections = chunk.getSections(); + int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); +@@ -935,8 +984,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + // Paper end - optimise random block ticking +- timings.chunkTicksBlocks.stopTiming(); // Paper +- gameprofilerfiller.pop(); ++ //timings.chunkTicksBlocks.stopTiming(); // Paper // Purpur ++ //gameprofilerfiller.pop(); // Purpur + } + + public Optional findLightningRod(BlockPos pos) { +@@ -944,7 +993,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + return holder.is(PoiTypes.LIGHTNING_ROD); + }, (blockposition1) -> { + return blockposition1.getY() == this.getHeight(Heightmap.Types.WORLD_SURFACE, blockposition1.getX(), blockposition1.getZ()) - 1; +- }, pos, 128, PoiManager.Occupancy.ANY); ++ }, pos, org.purpurmc.purpur.PurpurConfig.lightningRodRange, PoiManager.Occupancy.ANY); + + return optional.map((blockposition1) -> { + return blockposition1.above(1); +@@ -993,11 +1042,27 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (this.canSleepThroughNights()) { + if (!this.getServer().isSingleplayer() || this.getServer().isPublished()) { + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); +- MutableComponent ichatmutablecomponent; ++ Component ichatmutablecomponent; + + if (this.sleepStatus.areEnoughSleeping(i)) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.sleepSkippingNight.isBlank()) { ++ return; ++ } ++ if (!org.purpurmc.purpur.PurpurConfig.sleepSkippingNight.equalsIgnoreCase("default")) { ++ ichatmutablecomponent = io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.sleepSkippingNight)); ++ } else + ichatmutablecomponent = Component.translatable("sleep.skipping_night"); + } else { ++ if (org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent.isBlank()) { ++ return; ++ } ++ if (!org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent.equalsIgnoreCase("default")) { ++ ichatmutablecomponent = io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.sleepingPlayersPercent, ++ net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed("count", Integer.toString(this.sleepStatus.amountSleeping())), ++ net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed("total", Integer.toString(this.sleepStatus.sleepersNeeded(i))))); ++ } else ++ // Purpur end + ichatmutablecomponent = Component.translatable("sleep.players_sleeping", this.sleepStatus.amountSleeping(), this.sleepStatus.sleepersNeeded(i)); + } + +@@ -1136,6 +1201,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + private void resetWeatherCycle() { + // CraftBukkit start ++ if (this.purpurConfig.rainStopsAfterSleep) // Purpur + this.serverLevelData.setRaining(false, org.bukkit.event.weather.WeatherChangeEvent.Cause.SLEEP); // Paper - when passing the night + // If we stop due to everyone sleeping we should reset the weather duration to some other random value. + // Not that everyone ever manages to get the whole server to sleep at the same time.... +@@ -1143,6 +1209,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.serverLevelData.setRainTime(0); + } + // CraftBukkit end ++ if (this.purpurConfig.thunderStopsAfterSleep) // Purpur + this.serverLevelData.setThundering(false, org.bukkit.event.weather.ThunderChangeEvent.Cause.SLEEP); // Paper - when passing the night + // CraftBukkit start + // If we stop due to everyone sleeping we should reset the weather duration to some other random value. +@@ -1210,24 +1277,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Spigot end + // Paper start- timings + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); +- timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper +- try { ++ //timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper // Purpur ++ //try { // Purpur + // Paper end - timings + entity.setOldPosAndRot(); +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + + ++entity.tickCount; +- this.getProfiler().push(() -> { ++ /*this.getProfiler().push(() -> { // Purpur + return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); +- }); +- gameprofilerfiller.incrementCounter("tickNonPassenger"); ++ });*/ // Purpur ++ //gameprofilerfiller.incrementCounter("tickNonPassenger"); // Purpur + if (isActive) { // Paper - EAR 2 + TimingHistory.activatedEntityTicks++; + entity.tick(); + entity.postTick(); // CraftBukkit + } else { entity.inactiveTick(); } // Paper - EAR 2 +- this.getProfiler().pop(); +- } finally { timer.stopTiming(); } // Paper - timings ++ //this.getProfiler().pop(); // Purpur ++ //} finally { timer.stopTiming(); } // Paper - timings // Purpur + Iterator iterator = entity.getPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1250,17 +1317,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (passenger instanceof Player || this.entityTickList.contains(passenger)) { + // Paper - EAR 2 + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); +- co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper +- try { ++ //co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper // Purpur ++ //try { // Purpur + // Paper end + passenger.setOldPosAndRot(); + ++passenger.tickCount; +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push(() -> { ++ /*gameprofilerfiller.push(() -> { // Purpur + return BuiltInRegistries.ENTITY_TYPE.getKey(passenger.getType()).toString(); +- }); +- gameprofilerfiller.incrementCounter("tickPassenger"); ++ });*/ // Purpur ++ //gameprofilerfiller.incrementCounter("tickPassenger"); // Purpur + // Paper start - EAR 2 + if (isActive) { + passenger.rideTick(); +@@ -1272,7 +1339,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + vehicle.positionRider(passenger); + } + // Paper end - EAR 2 +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + Iterator iterator = passenger.getPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1281,7 +1348,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.tickPassenger(passenger, entity2); + } + +- } finally { timer.stopTiming(); }// Paper - EAR2 timings ++ //} finally { timer.stopTiming(); }// Paper - EAR2 timings // Purpur + } + } else { + passenger.stopRiding(); +@@ -1301,14 +1368,14 @@ public class ServerLevel extends Level implements WorldGenLevel { + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); + } + +- try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { ++ //try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { // Purpur + if (doFull) { + this.saveLevelData(); + } + +- this.timings.worldSaveChunks.startTiming(); // Paper ++ //this.timings.worldSaveChunks.startTiming(); // Paper // Purpur + if (!this.noSave()) chunkproviderserver.saveIncrementally(); +- this.timings.worldSaveChunks.stopTiming(); // Paper ++ //this.timings.worldSaveChunks.stopTiming(); // Paper // Purpur + + // Copied from save() + // CraftBukkit start - moved from MinecraftServer.saveChunks +@@ -1320,7 +1387,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); + } + // CraftBukkit end +- } ++ //} // Purpur + } + // Paper end + +@@ -1334,7 +1401,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + if (!savingDisabled) { + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); // CraftBukkit +- try (co.aikar.timings.Timing ignored = timings.worldSave.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = timings.worldSave.startTiming()) { // Paper // Purpur // Purpur + if (progressListener != null) { + progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); + } +@@ -1344,11 +1411,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + progressListener.progressStage(Component.translatable("menu.savingChunks")); + } + +- timings.worldSaveChunks.startTiming(); // Paper ++ //timings.worldSaveChunks.startTiming(); // Paper // Purpur + if (!close) chunkproviderserver.save(flush); // Paper - rewrite chunk system + if (close) chunkproviderserver.close(true); // Paper - rewrite chunk system +- timings.worldSaveChunks.stopTiming(); // Paper +- }// Paper ++ //timings.worldSaveChunks.stopTiming(); // Paper // Purpur ++ //}// Paper // Purpur + // Paper - rewrite chunk system - entity saving moved into ChunkHolder + + } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system +@@ -2619,7 +2686,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Spigot Start + if (entity.getBukkitEntity() instanceof org.bukkit.inventory.InventoryHolder && (!(entity instanceof ServerPlayer) || entity.getRemovalReason() != Entity.RemovalReason.KILLED)) { // SPIGOT-6876: closeInventory clears death message + // Paper start +- if (entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) { ++ if (!entity.level.purpurConfig.playerVoidTrading && entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) { // Purpur + merchant.getTrader().closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); + } + // Paper end +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 98df2463bf41fc736aa6a2b6ddf89e5abde6eb39..82402be7bbbf04388339c6a471946a72f09dfe3b 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -277,6 +277,11 @@ public class ServerPlayer extends Player { + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper + public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper + public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event ++ public boolean purpurClient = false; // Purpur ++ public boolean acceptingResourcePack = false; // Purpur ++ private boolean ramBar = false; // Purpur ++ private boolean tpsBar = false; // Purpur ++ private boolean compassBar = false; // Purpur + + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); +@@ -376,6 +381,7 @@ public class ServerPlayer extends Player { + this.bukkitPickUpLoot = true; + this.maxHealthCache = this.getMaxHealth(); + this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper ++ this.spawnInvulnerableTime = world.purpurConfig.playerSpawnInvulnerableTicks; // Purpur + } + + // Yes, this doesn't match Vanilla, but it's the best we can do for now. +@@ -515,6 +521,9 @@ public class ServerPlayer extends Player { + } + } + ++ if (nbt.contains("Purpur.RamBar")) { this.ramBar = nbt.getBoolean("Purpur.RamBar"); } // Purpur ++ if (nbt.contains("Purpur.TPSBar")) { this.tpsBar = nbt.getBoolean("Purpur.TPSBar"); } // Purpur ++ if (nbt.contains("Purpur.CompassBar")) { this.compassBar = nbt.getBoolean("Purpur.CompassBar"); } // Purpur + } + + @Override +@@ -581,6 +590,9 @@ public class ServerPlayer extends Player { + } + this.getBukkitEntity().setExtraData(nbt); // CraftBukkit + ++ nbt.putBoolean("Purpur.RamBar", this.ramBar); // Purpur ++ nbt.putBoolean("Purpur.TPSBar", this.tpsBar); // Purpur ++ nbt.putBoolean("Purpur.CompassBar", this.compassBar); // Purpur + } + + // CraftBukkit start - World fallback code, either respawn location or global spawn +@@ -709,6 +721,15 @@ public class ServerPlayer extends Player { + this.trackStartFallingPosition(); + this.trackEnteredOrExitedLavaOnVehicle(); + this.advancements.flushDirty(this); ++ ++ // Purpur start ++ if (this.level.purpurConfig.useNightVisionWhenRiding && this.getVehicle() != null && this.getVehicle().getRider() == this && this.level.getGameTime() % 100 == 0) { // 5 seconds ++ MobEffectInstance nightVision = this.getEffect(MobEffects.NIGHT_VISION); ++ if (nightVision == null || nightVision.getDuration() <= 300) { // 15 seconds ++ this.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 400, 0)); // 20 seconds ++ } ++ } ++ // Purpur end + } + + public void doTick() { +@@ -947,6 +968,7 @@ public class ServerPlayer extends Player { + })); + Team scoreboardteambase = this.getTeam(); + ++ if (org.purpurmc.purpur.PurpurConfig.deathMessageOnlyBroadcastToAffectedPlayer) this.sendSystemMessage(ichatbasecomponent); else // Purpur + if (scoreboardteambase != null && scoreboardteambase.getDeathMessageVisibility() != Team.Visibility.ALWAYS) { + if (scoreboardteambase.getDeathMessageVisibility() == Team.Visibility.HIDE_FOR_OTHER_TEAMS) { + this.server.getPlayerList().broadcastSystemToTeam(this, ichatbasecomponent); +@@ -1048,14 +1070,30 @@ public class ServerPlayer extends Player { + + } + ++ // Purpur start ++ public boolean isSpawnInvulnerable() { ++ return spawnInvulnerableTime > 0 || frozen; ++ } ++ // Purpur end ++ + @Override + public boolean hurt(DamageSource source, float amount) { + if (this.isInvulnerableTo(source)) { + return false; + } else { ++ // Purpur start ++ if (source.is(DamageTypeTags.IS_FALL)) { // Purpur ++ if (getRootVehicle() instanceof net.minecraft.world.entity.vehicle.AbstractMinecart && level.purpurConfig.minecartControllable && !level.purpurConfig.minecartControllableFallDamage) { ++ return false; ++ } ++ if (getRootVehicle() instanceof net.minecraft.world.entity.vehicle.Boat && !level.purpurConfig.boatsDoFallDamage) { ++ return false; ++ } ++ } ++ // Purpur end + boolean flag = this.server.isDedicatedServer() && this.isPvpAllowed() && source.is(DamageTypeTags.IS_FALL); + +- if (!flag && this.spawnInvulnerableTime > 0 && !source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) { ++ if (!flag && isSpawnInvulnerable() && !source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) { // Purpur + return false; + } else { + Entity entity = source.getEntity(); +@@ -1164,7 +1202,7 @@ public class ServerPlayer extends Player { + PortalInfo shapedetectorshape = this.findDimensionEntryPoint(worldserver); + + if (shapedetectorshape != null) { +- worldserver1.getProfiler().push("moving"); ++ //worldserver1.getProfiler().push("moving"); // Purpur + worldserver = shapedetectorshape.world; // CraftBukkit + if (worldserver == null) { } else // CraftBukkit - empty to fall through to null to event + if (resourcekey == LevelStem.OVERWORLD && worldserver.getTypeKey() == LevelStem.NETHER) { // CraftBukkit +@@ -1187,8 +1225,8 @@ public class ServerPlayer extends Player { + worldserver = ((CraftWorld) exit.getWorld()).getHandle(); + // CraftBukkit end + +- worldserver1.getProfiler().pop(); +- worldserver1.getProfiler().push("placing"); ++ //worldserver1.getProfiler().pop(); // Purpur ++ //worldserver1.getProfiler().push("placing"); // Purpur + if (true) { // CraftBukkit + this.isChangingDimension = true; // CraftBukkit - Set teleport invulnerability only if player changing worlds + +@@ -1199,6 +1237,7 @@ public class ServerPlayer extends Player { + playerlist.sendPlayerPermissionLevel(this); + worldserver1.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); + this.unsetRemoved(); ++ this.portalPos = io.papermc.paper.util.MCUtil.toBlockPosition(exit); // Purpur + + // CraftBukkit end + this.setLevel(worldserver); +@@ -1206,7 +1245,7 @@ public class ServerPlayer extends Player { + this.connection.teleport(exit); // CraftBukkit - use internal teleport without event + this.connection.resetPosition(); + worldserver.addDuringPortalTeleport(this); +- worldserver1.getProfiler().pop(); ++ //worldserver1.getProfiler().pop(); // Purpur + this.triggerDimensionChangeTriggers(worldserver1); + this.connection.send(new ClientboundPlayerAbilitiesPacket(this.getAbilities())); + playerlist.sendLevelInfo(this, worldserver); +@@ -1235,6 +1274,7 @@ public class ServerPlayer extends Player { + } + // Paper end + ++ this.spawnInvulnerableTime = worldserver.purpurConfig.playerSpawnInvulnerableTicks; // Purpur + return this; + } + } +@@ -1356,7 +1396,7 @@ public class ServerPlayer extends Player { + return entitymonster.isPreventingPlayerRest(this); + }); + +- if (!list.isEmpty()) { ++ if (!this.level.purpurConfig.playerSleepNearMonsters && !list.isEmpty()) { // Purpur + return Either.left(Player.BedSleepingProblem.NOT_SAFE); + } + } +@@ -1492,6 +1532,7 @@ public class ServerPlayer extends Player { + + @Override + public void openTextEdit(SignBlockEntity sign) { ++ if (level.purpurConfig.signAllowColors) this.connection.send(sign.getTranslatedUpdatePacket(textFilteringEnabled)); // Purpur + sign.setAllowedPlayerEditor(this.getUUID()); + this.connection.send(new ClientboundBlockUpdatePacket(this.level, sign.getBlockPos())); + this.connection.send(new ClientboundOpenSignEditorPacket(sign.getBlockPos())); +@@ -1728,6 +1769,26 @@ public class ServerPlayer extends Player { + this.lastSentExp = -1; // CraftBukkit - Added to reset + } + ++ // Purpur start ++ public void sendActionBarMessage(@Nullable String message) { ++ if (message != null && !message.isEmpty()) { ++ sendActionBarMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message)); ++ } ++ } ++ ++ public void sendActionBarMessage(@Nullable net.kyori.adventure.text.Component message) { ++ if (message != null) { ++ sendActionBarMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message)); ++ } ++ } ++ ++ public void sendActionBarMessage(@Nullable Component message) { ++ if (message != null) { ++ displayClientMessage(message, true); ++ } ++ } ++ // Purpur end ++ + @Override + public void displayClientMessage(Component message, boolean overlay) { + this.sendSystemMessage(message, overlay); +@@ -2027,6 +2088,7 @@ public class ServerPlayer extends Player { + } + + public void sendTexturePack(String url, String hash, boolean required, @Nullable Component resourcePackPrompt) { ++ this.acceptingResourcePack = true; // Purpur + this.connection.send(new ClientboundResourcePackPacket(url, hash, required, resourcePackPrompt)); + } + +@@ -2041,8 +2103,63 @@ public class ServerPlayer extends Player { + + public void resetLastActionTime() { + this.lastActionTime = Util.getMillis(); ++ this.setAfk(false); // Purpur + } + ++ // Purpur Start ++ private boolean isAfk = false; ++ ++ @Override ++ public void setAfk(boolean afk) { ++ if (this.isAfk == afk) { ++ return; ++ } ++ ++ String msg = afk ? org.purpurmc.purpur.PurpurConfig.afkBroadcastAway : org.purpurmc.purpur.PurpurConfig.afkBroadcastBack; ++ ++ org.purpurmc.purpur.event.PlayerAFKEvent event = new org.purpurmc.purpur.event.PlayerAFKEvent(this.getBukkitEntity(), afk, this.level.purpurConfig.idleTimeoutKick, msg, !Bukkit.isPrimaryThread()); ++ if (!event.callEvent() || event.shouldKick()) { ++ return; ++ } ++ ++ this.isAfk = afk; ++ ++ if (!afk) { ++ resetLastActionTime(); ++ } ++ ++ msg = event.getBroadcastMsg(); ++ if (msg != null && !msg.isEmpty()) { ++ server.getPlayerList().broadcastMiniMessage(String.format(msg, this.getGameProfile().getName()), false); ++ } ++ ++ if (this.level.purpurConfig.idleTimeoutUpdateTabList) { ++ String scoreboardName = getScoreboardName(); ++ String playerListName = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().serialize(getBukkitEntity().playerListName()); ++ String[] split = playerListName.split(scoreboardName); ++ String prefix = (split.length > 0 ? split[0] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix, ""); ++ String suffix = (split.length > 1 ? split[1] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, ""); ++ if (afk) { ++ getBukkitEntity().setPlayerListName(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix + prefix + scoreboardName + suffix + org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, true); ++ } else { ++ getBukkitEntity().setPlayerListName(prefix + scoreboardName + suffix); ++ } ++ } ++ ++ ((ServerLevel) this.level).updateSleepingPlayerList(); ++ } ++ ++ @Override ++ public boolean isAfk() { ++ return this.isAfk; ++ } ++ ++ @Override ++ public boolean canBeCollidedWith() { ++ return !this.isAfk() && super.canBeCollidedWith(); ++ } ++ // Purpur End ++ + public ServerStatsCounter getStats() { + return this.stats; + } +@@ -2514,8 +2631,16 @@ public class ServerPlayer extends Player { + + @Override + public boolean isImmobile() { +- return super.isImmobile() || (this.connection != null && this.connection.isDisconnected()); // Paper ++ return super.isImmobile() || frozen || (this.connection != null && this.connection.isDisconnected()); // Paper // Purpur ++ } ++ ++ // Purpur start ++ private boolean frozen = false; ++ ++ public void setFrozen(boolean frozen) { ++ this.frozen = frozen; + } ++ // Purpur end + + @Override + public Scoreboard getScoreboard() { +@@ -2564,4 +2689,50 @@ public class ServerPlayer extends Player { + return (CraftPlayer) super.getBukkitEntity(); + } + // CraftBukkit end ++ ++ // Purpur start ++ public void teleport(Location to) { ++ this.ejectPassengers(); ++ this.stopRiding(true); ++ ++ if (this.isSleeping()) { ++ this.stopSleepInBed(true, false); ++ } ++ ++ if (this.containerMenu != this.inventoryMenu) { ++ this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.TELEPORT); ++ } ++ ++ ServerLevel toLevel = ((CraftWorld) to.getWorld()).getHandle(); ++ if (this.level == toLevel) { ++ this.connection.internalTeleport(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch(), java.util.EnumSet.noneOf(net.minecraft.world.entity.RelativeMovement.class)); ++ } else { ++ this.server.getPlayerList().respawn(this, toLevel, true, to, !toLevel.paperConfig().environment.disableTeleportationSuffocationCheck, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.DEATH); ++ } ++ } ++ ++ public boolean ramBar() { ++ return this.ramBar; ++ } ++ ++ public void ramBar(boolean ramBar) { ++ this.ramBar = ramBar; ++ } ++ ++ public boolean tpsBar() { ++ return this.tpsBar; ++ } ++ ++ public void tpsBar(boolean tpsBar) { ++ this.tpsBar = tpsBar; ++ } ++ ++ public boolean compassBar() { ++ return this.compassBar; ++ } ++ ++ public void compassBar(boolean compassBar) { ++ this.compassBar = compassBar; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index 1d33c02088c150189d7f4b0aa27f6a1de96b11cf..75f29f6dddf50ccf7ef43ecfa602ccade3c9004d 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -397,6 +397,7 @@ public class ServerPlayerGameMode { + } else {capturedBlockEntity = true;} // Paper end + return false; + } ++ if (this.player.level.purpurConfig.slabHalfBreak && this.player.isShiftKeyDown() && iblockdata.getBlock() instanceof net.minecraft.world.level.block.SlabBlock && ((net.minecraft.world.level.block.SlabBlock) iblockdata.getBlock()).halfBreak(iblockdata, pos, this.player)) return true; // Purpur + } + // CraftBukkit end + +@@ -427,7 +428,7 @@ public class ServerPlayerGameMode { + + ItemStack mainHandStack = null; // Paper + boolean isCorrectTool = false; // Paper +- if (this.isCreative()) { ++ if (this.isCreative() || (this.level.purpurConfig.shulkerBoxAllowOversizedStacks && block instanceof net.minecraft.world.level.block.ShulkerBoxBlock)) { // Purpur + // return true; // CraftBukkit + } else { + ItemStack itemstack = this.player.getMainHandItem(); +@@ -516,6 +517,7 @@ public class ServerPlayerGameMode { + public InteractionHand interactHand; + public ItemStack interactItemStack; + public InteractionResult useItemOn(ServerPlayer player, Level world, ItemStack stack, InteractionHand hand, BlockHitResult hitResult) { ++ if (shiftClickMended(stack)) return InteractionResult.SUCCESS; // Purpur + BlockPos blockposition = hitResult.getBlockPos(); + BlockState iblockdata = world.getBlockState(blockposition); + InteractionResult enuminteractionresult = InteractionResult.PASS; +@@ -576,7 +578,7 @@ public class ServerPlayerGameMode { + boolean flag1 = player.isSecondaryUseActive() && flag; + ItemStack itemstack1 = stack.copy(); + +- if (!flag1) { ++ if (!flag1 || (player.level.purpurConfig.composterBulkProcess && iblockdata.is(Blocks.COMPOSTER))) { // Purpur + enuminteractionresult = iblockdata.use(world, player, hand, hitResult); + + if (enuminteractionresult.consumesAction()) { +@@ -612,4 +614,18 @@ public class ServerPlayerGameMode { + public void setLevel(ServerLevel world) { + this.level = world; + } ++ ++ // Purpur start ++ public boolean shiftClickMended(ItemStack itemstack) { ++ if (this.player.level.purpurConfig.shiftRightClickRepairsMendingPoints > 0 && this.player.isShiftKeyDown() && this.player.getBukkitEntity().hasPermission("purpur.mending_shift_click")) { ++ int points = Math.min(this.player.totalExperience, this.player.level.purpurConfig.shiftRightClickRepairsMendingPoints); ++ if (points > 0 && itemstack.isDamaged() && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.MENDING, itemstack) > 0) { ++ this.player.giveExperiencePoints(-points); ++ this.player.level.addFreshEntity(new net.minecraft.world.entity.ExperienceOrb(this.player.level, this.player.getX(), this.player.getY(), this.player.getZ(), points, org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN, this.player, this.player)); ++ return true; ++ } ++ } ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 877498729c66de9aa6a27c9148f7494d7895615c..acd7468ee3c86d3456e96e4ec3d7e6a4c612e89d 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -297,6 +297,7 @@ public class WorldGenRegion implements WorldGenLevel { + return true; + } else { + // Paper start ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressSetBlockFarChunk) // Purpur + if (!hasSetFarWarned) { + Util.logAndPauseIfInIde("Detected setBlock in a far chunk [" + i + ", " + j + "], pos: " + pos + ", status: " + this.generatingStatus + (this.currentlyGenerating == null ? "" : ", currently generating: " + (String) this.currentlyGenerating.get())); + hasSetFarWarned = true; +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 9d2d72fe48b69be2f6ebe74309673a3a4e51eae4..af8cb1f1f0c128923495f51e4828003133ce766b 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -263,6 +263,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private long keepAliveTime = Util.getMillis(); + private boolean keepAlivePending; + private long keepAliveChallenge; ++ private it.unimi.dsi.fastutil.longs.LongList keepAlives = new it.unimi.dsi.fastutil.longs.LongArrayList(); // Purpur + // CraftBukkit start - multithreaded fields + private final AtomicInteger chatSpamTickCount = new AtomicInteger(); + private final java.util.concurrent.atomic.AtomicInteger tabSpamLimiter = new java.util.concurrent.atomic.AtomicInteger(); // Paper - configurable tab spam limits +@@ -340,6 +341,20 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private boolean justTeleported = false; + private boolean hasMoved; // Spigot + ++ // Purpur start ++ private final com.google.common.cache.LoadingCache kickPermissionCache = com.google.common.cache.CacheBuilder.newBuilder() ++ .maximumSize(1000) ++ .expireAfterWrite(1, java.util.concurrent.TimeUnit.MINUTES) ++ .build( ++ new com.google.common.cache.CacheLoader<>() { ++ @Override ++ public Boolean load(CraftPlayer player) { ++ return player.hasPermission("purpur.bypassIdleKick"); ++ } ++ } ++ ); ++ // Purpur end ++ + public CraftPlayer getCraftPlayer() { + return (this.player == null) ? null : (CraftPlayer) this.player.getBukkitEntity(); + } +@@ -395,12 +410,27 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.aboveGroundVehicleTickCount = 0; + } + +- this.server.getProfiler().push("keepAlive"); ++ //this.server.getProfiler().push("keepAlive"); // Purpur + // Paper Start - give clients a longer time to respond to pings as per pre 1.12.2 timings + // This should effectively place the keepalive handling back to "as it was" before 1.12.2 + long currentTime = Util.getMillis(); + long elapsedTime = currentTime - this.keepAliveTime; + ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useAlternateKeepAlive) { ++ if (elapsedTime >= 1000L) { // 1 second ++ if (!processedDisconnect && keepAlives.size() * 1000L >= KEEPALIVE_LIMIT) { ++ LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); ++ disconnect(Component.translatable("disconnect.timeout"), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); ++ } else { ++ keepAliveTime = currentTime; // hijack this field for 1 second intervals ++ keepAlives.add(currentTime); // currentTime is ID ++ send(new ClientboundKeepAlivePacket(currentTime)); ++ } ++ } ++ } else ++ // Purpur end ++ + if (this.keepAlivePending) { + if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected + ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info +@@ -416,7 +446,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + // Paper end + +- this.server.getProfiler().pop(); ++ //this.server.getProfiler().pop(); // Purpur + // CraftBukkit start + for (int spam; (spam = this.chatSpamTickCount.get()) > 0 && !this.chatSpamTickCount.compareAndSet(spam, spam - 1); ) ; + if (tabSpamLimiter.get() > 0) tabSpamLimiter.getAndDecrement(); // Paper - split to seperate variable +@@ -433,6 +463,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + + if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) (this.server.getPlayerIdleTimeout() * 1000 * 60) && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits. ++ // Purpur start ++ this.player.setAfk(true); ++ if (!this.player.level.purpurConfig.idleTimeoutKick || (!Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")) && kickPermissionCache.getUnchecked(this.player.getBukkitEntity()))) { ++ return; ++ } ++ // Purpur end + this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854 + this.disconnect(Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause + } +@@ -744,6 +780,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); + ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur ++ + // Skip the first time we do this + if (true) { // Spigot - don't skip any move events + Location oldTo = to.clone(); +@@ -820,6 +858,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + if (packet.getId() == this.awaitingTeleport) { + if (this.awaitingPositionFromClient == null) { + this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause ++ ServerGamePacketListenerImpl.LOGGER.warn("Disconnected on accept teleport packet. Was not expecting position data from client at this time"); // Purpur + return; + } + +@@ -1224,10 +1263,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + int maxBookPageSize = io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.pageMax; + double multiplier = Math.max(0.3D, Math.min(1D, io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.totalMultiplier)); + long byteAllowed = maxBookPageSize; ++ ItemStack itemstack = this.player.getInventory().getItem(packet.getSlot()); // Purpur + for (String testString : pageList) { + int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + if (byteLength > 256 * 4) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!"); ++ org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent event = new org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent(player.getBukkitEntity(), itemstack.asBukkitCopy()); if (event.shouldKickPlayer()) // Purpur + server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + return; + } +@@ -1251,6 +1292,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + if (byteTotal > byteAllowed) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); ++ org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent event = new org.purpurmc.purpur.event.player.PlayerBookTooLargeEvent(player.getBukkitEntity(), itemstack.asBukkitCopy()); if (event.shouldKickPlayer()) // Purpur + server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + return; + } +@@ -1304,13 +1346,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + itemstack1.setTag(nbttagcompound.copy()); + } + ++ // Purpur start ++ boolean hasPerm = getCraftPlayer().hasPermission("purpur.book.color.edit") || getCraftPlayer().hasPermission("purpur.book.color.sign"); + itemstack1.addTagElement("author", StringTag.valueOf(this.player.getName().getString())); + if (this.player.isTextFilteringEnabled()) { +- itemstack1.addTagElement("title", StringTag.valueOf(title.filteredOrEmpty())); ++ itemstack1.addTagElement("title", StringTag.valueOf(color(title.filteredOrEmpty(), hasPerm))); + } else { +- itemstack1.addTagElement("filtered_title", StringTag.valueOf(title.filteredOrEmpty())); +- itemstack1.addTagElement("title", StringTag.valueOf(title.raw())); ++ itemstack1.addTagElement("filtered_title", StringTag.valueOf(color(title.filteredOrEmpty(), hasPerm))); ++ itemstack1.addTagElement("title", StringTag.valueOf(color(title.raw(), hasPerm))); + } ++ // Purpur end + + this.updateBookPages(pages, (s) -> { + return Component.Serializer.toJson(Component.literal(s)); +@@ -1322,10 +1367,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private void updateBookPages(List list, UnaryOperator unaryoperator, ItemStack itemstack, int slot, ItemStack handItem) { // CraftBukkit + ListTag nbttaglist = new ListTag(); + ++ // Purpur start ++ boolean hasPerm = getCraftPlayer().hasPermission("purpur.book.color.edit"); + if (this.player.isTextFilteringEnabled()) { +- Stream stream = list.stream().map((filteredtext) -> { // CraftBukkit - decompile error +- return StringTag.valueOf((String) unaryoperator.apply(filteredtext.filteredOrEmpty())); ++ Stream stream = list.stream().map(s -> color(s.filteredOrEmpty(), hasPerm, false)).map((s) -> { // CraftBukkit - decompile error ++ return StringTag.valueOf((String) unaryoperator.apply(s)); + }); ++ // Purpur end + + Objects.requireNonNull(nbttaglist); + stream.forEach(nbttaglist::add); +@@ -1335,11 +1383,11 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + for (int j = list.size(); i < j; ++i) { + FilteredText filteredtext = (FilteredText) list.get(i); +- String s = filteredtext.raw(); ++ String s = color(filteredtext.raw(), hasPerm, false); // Purpur + + nbttaglist.add(StringTag.valueOf((String) unaryoperator.apply(s))); + if (filteredtext.isFiltered()) { +- nbttagcompound.putString(String.valueOf(i), (String) unaryoperator.apply(filteredtext.filteredOrEmpty())); ++ nbttagcompound.putString(String.valueOf(i), (String) unaryoperator.apply((String) color(filteredtext.filteredOrEmpty(), hasPerm, false))); // Purpur + } + } + +@@ -1352,6 +1400,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.player.getInventory().setItem(slot, CraftEventFactory.handleEditBookEvent(player, slot, handItem, itemstack)); // CraftBukkit // Paper - Don't ignore result (see other callsite for handleEditBookEvent) + } + ++ // Purpur start ++ private String color(String str, boolean hasPerm) { ++ return color(str, hasPerm, true); ++ } ++ ++ private String color(String str, boolean hasPerm, boolean parseHex) { ++ return hasPerm ? org.bukkit.ChatColor.color(str, parseHex) : str; ++ } ++ // Purpur end ++ + @Override + public void handleEntityTagQuery(ServerboundEntityTagQuery packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); +@@ -1381,8 +1439,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + @Override + public void handleMovePlayer(ServerboundMovePlayerPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); +- if (ServerGamePacketListenerImpl.containsInvalidValues(packet.getX(0.0D), packet.getY(0.0D), packet.getZ(0.0D), packet.getYRot(0.0F), packet.getXRot(0.0F))) { ++ // Purpur start ++ boolean invalidX = Double.isNaN(packet.getX(0.0D)); ++ boolean invalidY = Double.isNaN(packet.getY(0.0D)); ++ boolean invalidZ = Double.isNaN(packet.getZ(0.0D)); ++ boolean invalidYaw = !Floats.isFinite(packet.getYRot(0.0F)); ++ boolean invalidPitch = !Floats.isFinite(packet.getXRot(0.0F)); ++ if (invalidX || invalidY || invalidZ || invalidYaw || invalidPitch) { + this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause ++ ServerGamePacketListenerImpl.LOGGER.warn(String.format("Disconnected on move player packet. Invalid data: x=%b, y=%b, z=%b, yaw=%b, pitch=%b", invalidX, invalidY, invalidZ, invalidYaw, invalidPitch)); ++ // Purpur end + } else { + ServerLevel worldserver = this.player.getLevel(); + +@@ -1548,7 +1614,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot + flag2 = true; // Paper - diff on change, this should be moved wrongly +- ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); ++ ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!, ({})", this.player.getName().getString(), d11); // Purpur + } + + this.player.absMoveTo(d0, d1, d2, f, f1); +@@ -1599,6 +1665,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); + ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur ++ + // Skip the first time we do this + if (from.getX() != Double.MAX_VALUE) { + Location oldTo = to.clone(); +@@ -1638,6 +1706,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.player.resetFallDistance(); + } + ++ // Purpur Start ++ if (this.player.level.purpurConfig.dontRunWithScissors && this.player.isSprinting() && !(this.player.level.purpurConfig.ignoreScissorsInWater && this.player.isInWater()) && !(this.player.level.purpurConfig.ignoreScissorsInLava && this.player.isInLava()) && (isScissor(this.player.getItemInHand(InteractionHand.MAIN_HAND)) || isScissor(this.player.getItemInHand(InteractionHand.OFF_HAND))) && (int) (Math.random() * 10) == 0) { ++ this.player.hurt(this.player.damageSources().magic(), (float) this.player.level.purpurConfig.scissorsRunningDamage); ++ if (!org.purpurmc.purpur.PurpurConfig.dontRunWithScissors.isBlank()) this.player.sendActionBarMessage(org.purpurmc.purpur.PurpurConfig.dontRunWithScissors); ++ } ++ // Purpur End ++ + this.player.checkMovementStatistics(this.player.getX() - d3, this.player.getY() - d4, this.player.getZ() - d5); + this.lastGoodX = this.player.getX(); + this.lastGoodY = this.player.getY(); +@@ -1671,6 +1746,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + // Paper end - optimise out extra getCubes + ++ // Purpur start ++ public boolean isScissor(ItemStack stack) { ++ return stack.is(Items.SHEARS) && (stack.getTag() == null || stack.getTag().getInt("CustomModelData") == 0); ++ } ++ // Purpur end ++ + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) { + Iterable iterable = world.getCollisions(this.player, this.player.getBoundingBox().deflate(9.999999747378752E-6D)); + VoxelShape voxelshape = Shapes.create(box.deflate(9.999999747378752E-6D)); +@@ -2015,6 +2096,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + boolean cancelled; + if (movingobjectposition == null || movingobjectposition.getType() != HitResult.Type.BLOCK) { ++ if (this.player.gameMode.shiftClickMended(itemstack)) return; // Purpur + org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_AIR, itemstack, enumhand); + cancelled = event.useItemInHand() == Event.Result.DENY; + } else { +@@ -2068,12 +2150,21 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + @Override + public void handleResourcePackResponse(ServerboundResourcePackPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); ++ // Purpur start ++ if (player.level.purpurConfig.playerInvulnerableWhileAcceptingResourcePack && !this.player.acceptingResourcePack) { ++ ServerGamePacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack packet exploitation attempt", this.player.getName()); ++ this.disconnect(Component.translatable("multiplayer.texturePrompt.failure.line1"), org.bukkit.event.player.PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // "Server resource pack couldn't be applied" ++ return; ++ } ++ // Purpur end + if (packet.getAction() == ServerboundResourcePackPacket.Action.DECLINED && this.server.isResourcePackRequired()) { + ServerGamePacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack rejection", this.player.getGameProfile().getName()); // Paper - Don't print component in resource pack rejection message + this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"), org.bukkit.event.player.PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // Paper - add cause + } + // Paper start + PlayerResourcePackStatusEvent.Status packStatus = PlayerResourcePackStatusEvent.Status.values()[packet.action.ordinal()]; ++ if (player.level.purpurConfig.playerInvulnerableWhileAcceptingResourcePack) player.setFrozen(packStatus == PlayerResourcePackStatusEvent.Status.ACCEPTED); // Purpur ++ this.player.acceptingResourcePack = packStatus == PlayerResourcePackStatusEvent.Status.ACCEPTED; // Purpur + player.getBukkitEntity().setResourcePackStatus(packStatus); + this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packStatus)); // CraftBukkit + // Paper end +@@ -2370,7 +2461,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + do { + instant1 = (Instant) this.lastChatTimeStamp.get(); + if (timestamp.isBefore(instant1)) { +- return false; ++ return !org.purpurmc.purpur.PurpurConfig.kickForOutOfOrderChat; // Purpur + } + } while (!this.lastChatTimeStamp.compareAndSet(instant1, timestamp)); + +@@ -2507,7 +2598,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + } + // Paper End +- co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper // Purpur + if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot + this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s); + +@@ -2517,7 +2608,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.cserver.getPluginManager().callEvent(event); + + if (event.isCancelled()) { +- co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper // Purpur + return; + } + +@@ -2530,7 +2621,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + java.util.logging.Logger.getLogger(ServerGamePacketListenerImpl.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + return; + } finally { +- co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper ++ //co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper // Purpur + } + } + // CraftBukkit end +@@ -2796,6 +2887,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + AABB axisalignedbb = entity.getBoundingBox(); + + if (axisalignedbb.distanceToSqr(this.player.getEyePosition()) < ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) { ++ if (entity instanceof Mob mob) mob.ticksSinceLastInteraction = 0; // Purpur + packet.dispatch(new ServerboundInteractPacket.Handler() { + private void performInteraction(InteractionHand enumhand, ServerGamePacketListenerImpl.EntityInteraction playerconnection_a, PlayerInteractEntityEvent event) { // CraftBukkit + ItemStack itemstack = ServerGamePacketListenerImpl.this.player.getItemInHand(enumhand); +@@ -2809,6 +2901,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event); + ++ player.processClick(enumhand); // Purpur ++ + // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a + if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { + entity.getEntityData().resendPossiblyDesyncedEntity(player); // Paper - The entire mob gets deleted, so resend it. +@@ -3360,6 +3454,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + } + } ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.fixNetworkSerializedItemsInCreative) { ++ var tag = itemstack.getTagElement("Purpur.OriginalItem"); ++ if (tag != null) itemstack = ItemStack.of(tag); ++ } ++ // Purpur end + + boolean flag1 = packet.getSlotNum() >= 1 && packet.getSlotNum() <= 45; + boolean flag2 = itemstack.isEmpty() || itemstack.getDamageValue() >= 0 && itemstack.getCount() <= 64 && !itemstack.isEmpty(); +@@ -3466,11 +3566,17 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + for (int i = 0; i < signText.size(); ++i) { + FilteredText filteredtext = (FilteredText) signText.get(i); + +- if (this.player.isTextFilteringEnabled()) { +- lines.add(net.kyori.adventure.text.Component.text(SharedConstants.filterText(filteredtext.filteredOrEmpty()))); // Paper - adventure ++ // Purpur start ++ String line = SharedConstants.filterText(this.player.isTextFilteringEnabled() ? filteredtext.filteredOrEmpty() : filteredtext.raw()); ++ if (worldserver.purpurConfig.signAllowColors) { ++ if (player.hasPermission("purpur.sign.color")) line = line.replaceAll("(?i)&([0-9a-fr])", "\u00a7$1"); ++ if (player.hasPermission("purpur.sign.style")) line = line.replaceAll("(?i)&([l-or])", "\u00a7$1"); ++ if (player.hasPermission("purpur.sign.magic")) line = line.replaceAll("(?i)&([kr])", "\u00a7$1"); ++ lines.add(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(line)); + } else { +- lines.add(net.kyori.adventure.text.Component.text(SharedConstants.filterText(filteredtext.raw()))); // Paper - adventure ++ lines.add(net.kyori.adventure.text.Component.text(line)); + } ++ // Purpur end + } + SignChangeEvent event = new SignChangeEvent((org.bukkit.craftbukkit.block.CraftBlock) player.getWorld().getBlockAt(x, y, z), this.player.getBukkitEntity(), lines); + this.cserver.getPluginManager().callEvent(event); +@@ -3492,6 +3598,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + @Override + public void handleKeepAlive(ServerboundKeepAlivePacket packet) { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.useAlternateKeepAlive) { ++ long id = packet.getId(); ++ if (keepAlives.size() > 0 && keepAlives.contains(id)) { ++ int ping = (int) (Util.getMillis() - id); ++ player.latency = (player.latency * 3 + ping) / 4; ++ keepAlives.clear(); // we got a valid response, lets roll with it and forget the rest ++ } ++ } else ++ // Purpur end + //PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // CraftBukkit // Paper - This shouldn't be on the main thread + if (this.keepAlivePending && packet.getId() == this.keepAliveChallenge) { + int i = (int) (Util.getMillis() - this.keepAliveTime); +@@ -3542,6 +3658,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private static final ResourceLocation CUSTOM_UNREGISTER = new ResourceLocation("unregister"); + + private static final ResourceLocation MINECRAFT_BRAND = new ResourceLocation("brand"); // Paper - Brand support ++ private static final ResourceLocation PURPUR_CLIENT = new ResourceLocation("purpur", "client"); // Purpur + + @Override + public void handleCustomPayload(ServerboundCustomPayloadPacket packet) { +@@ -3566,6 +3683,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t unregister custom payload", ex); + this.disconnect("Invalid payload UNREGISTER!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause + } ++ // Purpur start ++ } else if (packet.identifier.equals(PURPUR_CLIENT)) { ++ try { ++ player.purpurClient = true; ++ } catch (Exception ignore) { ++ } ++ // Purpur end + } else { + try { + byte[] data = new byte[packet.data.readableBytes()]; +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 2ff578e4a953ffcf5176815ba8e3f06f73499989..f719f8aafe7c75e2ef8fcb05f556a8d6bd94b9a0 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -220,6 +220,8 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + return false; + } + ++ if (true) return org.purpurmc.purpur.PurpurConfig.usernameValidCharactersPattern.matcher(in).matches(); // Purpur ++ + for (int i = 0, len = in.length(); i < len; ++i) { + char c = in.charAt(i); + +@@ -339,7 +341,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + ServerLoginPacketListenerImpl.this.gameProfile = gameprofile; + ServerLoginPacketListenerImpl.this.state = ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT; + } else { +- ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.unverified_username")); ++ ServerLoginPacketListenerImpl.this.disconnect(org.purpurmc.purpur.PurpurConfig.unverifiedUsername.equals("default") ? Component.translatable("multiplayer.disconnect.unverified_username") : io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.purpurmc.purpur.PurpurConfig.unverifiedUsername))); // Purpur + ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", gameprofile.getName()); + } + } catch (AuthenticationUnavailableException authenticationunavailableexception) { +diff --git a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +index 2c13147bc063a09bb7907d6f90c3a1e811a09eb1..3edb0c392cec7fdabebad81da1a8b06a700fbfca 100644 +--- a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +@@ -153,6 +153,7 @@ public class ServerStatusPacketListenerImpl implements ServerStatusPacketListene + this.connection.send(new ClientboundStatusResponsePacket(ping)); + // CraftBukkit end + */ ++ if (MinecraftServer.getServer().getStatus().version().isEmpty()) return; // Purpur - do not respond to pings before we know the protocol version + com.destroystokyo.paper.network.StandardPaperServerListPingEventImpl.processRequest(MinecraftServer.getServer(), this.connection); + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java b/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java +index 9ddbfcf80d9a381dace78a62880f85a4d767e0eb..7383c7d3820dce06108eaafd236a7c6c06a10a42 100644 +--- a/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java ++++ b/src/main/java/net/minecraft/server/packs/resources/ResourceManagerReloadListener.java +@@ -9,11 +9,11 @@ public interface ResourceManagerReloadListener extends PreparableReloadListener + @Override + default CompletableFuture reload(PreparableReloadListener.PreparationBarrier synchronizer, ResourceManager manager, ProfilerFiller prepareProfiler, ProfilerFiller applyProfiler, Executor prepareExecutor, Executor applyExecutor) { + return synchronizer.wait(Unit.INSTANCE).thenRunAsync(() -> { +- applyProfiler.startTick(); +- applyProfiler.push("listener"); ++ //applyProfiler.startTick(); // Purpur ++ //applyProfiler.push("listener"); // Purpur + this.onResourceManagerReload(manager); +- applyProfiler.pop(); +- applyProfiler.endTick(); ++ //applyProfiler.pop(); // Purpur ++ //applyProfiler.endTick(); // Purpur + }, applyExecutor); + } + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 5c21de0d48fba88c3164b72e0eb624706b683fab..c27e455321951e76e4818fec0e64301f5620dbf6 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -469,6 +469,7 @@ public abstract class PlayerList { + scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); + } + // Paper end ++ org.purpurmc.purpur.task.BossBarTask.addToAll(player); // Purpur + // CraftBukkit - Moved from above, added world + PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ()); + } +@@ -578,6 +579,8 @@ public abstract class PlayerList { + } + public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer, net.kyori.adventure.text.Component leaveMessage) { + // Paper end ++ org.purpurmc.purpur.task.BossBarTask.removeFromAll(entityplayer.getBukkitEntity()); // Purpur ++ + ServerLevel worldserver = entityplayer.getLevel(); + + entityplayer.awardStat(Stats.LEAVE_GAME); +@@ -731,7 +734,7 @@ public abstract class PlayerList { + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure + } else { + // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; +- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { ++ if (this.players.size() >= this.maxPlayers && !(player.hasPermission("purpur.joinfullserver") || this.canBypassPlayerLimit(gameprofile))) { // Purpur + event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } +@@ -969,6 +972,8 @@ public abstract class PlayerList { + } + // Paper end + ++ entityplayer1.spawnInvulnerableTime = entityplayer1.level.purpurConfig.playerSpawnInvulnerableTicks; // Purpur ++ + // CraftBukkit end + return entityplayer1; + } +@@ -1029,6 +1034,20 @@ public abstract class PlayerList { + } + // CraftBukkit end + ++ // Purpur Start ++ public void broadcastMiniMessage(@Nullable String message, boolean overlay) { ++ if (message != null && !message.isEmpty()) { ++ this.broadcastMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message), overlay); ++ } ++ } ++ ++ public void broadcastMessage(@Nullable net.kyori.adventure.text.Component message, boolean overlay) { ++ if (message != null) { ++ this.broadcastSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message), overlay); ++ } ++ } ++ // Purpur end ++ + public void broadcastAll(Packet packet, ResourceKey dimension) { + Iterator iterator = this.players.iterator(); + +@@ -1132,6 +1151,7 @@ public abstract class PlayerList { + } else { + b0 = (byte) (24 + permissionLevel); + } ++ if (b0 < 28 && player.getBukkitEntity().hasPermission("purpur.debug.f3n")) b0 = 28; // Purpur + + player.connection.send(new ClientboundEntityEventPacket(player, b0)); + } +@@ -1140,6 +1160,27 @@ public abstract class PlayerList { + player.getBukkitEntity().recalculatePermissions(); // CraftBukkit + this.server.getCommands().sendCommands(player); + } // Paper ++ ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.enderChestSixRows && org.purpurmc.purpur.PurpurConfig.enderChestPermissionRows) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity bukkit = player.getBukkitEntity(); ++ if (bukkit.hasPermission("purpur.enderchest.rows.six")) { ++ player.sixRowEnderchestSlotCount = 54; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.five")) { ++ player.sixRowEnderchestSlotCount = 45; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.four")) { ++ player.sixRowEnderchestSlotCount = 36; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.three")) { ++ player.sixRowEnderchestSlotCount = 27; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.two")) { ++ player.sixRowEnderchestSlotCount = 18; ++ } else if (bukkit.hasPermission("purpur.enderchest.rows.one")) { ++ player.sixRowEnderchestSlotCount = 9; ++ } ++ } else { ++ player.sixRowEnderchestSlotCount = -1; ++ } ++ //Purpur end + } + + public boolean isWhiteListed(GameProfile profile) { +@@ -1201,7 +1242,7 @@ public abstract class PlayerList { + + public void saveAll(int interval) { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main +- MinecraftTimings.savePlayers.startTiming(); // Paper ++ //MinecraftTimings.savePlayers.startTiming(); // Paper // Purpur + int numSaved = 0; + long now = MinecraftServer.currentTick; + for (int i = 0; i < this.players.size(); ++i) { +@@ -1212,7 +1253,7 @@ public abstract class PlayerList { + } + // Paper end + } +- MinecraftTimings.savePlayers.stopTiming(); // Paper ++ //MinecraftTimings.savePlayers.stopTiming(); // Paper // Purpur + return null; }); // Paper - ensure main + } + +diff --git a/src/main/java/net/minecraft/server/players/SleepStatus.java b/src/main/java/net/minecraft/server/players/SleepStatus.java +index 823efad652d8ff9e96b99375b102fef6f017716e..60f89d7c77a5e792e21e93e35ed1670bd565799a 100644 +--- a/src/main/java/net/minecraft/server/players/SleepStatus.java ++++ b/src/main/java/net/minecraft/server/players/SleepStatus.java +@@ -19,7 +19,7 @@ public class SleepStatus { + + public boolean areEnoughDeepSleeping(int percentage, List players) { + // CraftBukkit start +- int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping; }).count(); ++ int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping || (eh.level.purpurConfig.idleTimeoutCountAsSleeping && eh.isAfk()); }).count(); // Purpur + boolean anyDeepSleep = players.stream().anyMatch(Player::isSleepingLongEnough); + + return anyDeepSleep && j >= this.sleepersNeeded(percentage); +@@ -52,7 +52,7 @@ public class SleepStatus { + + if (!entityplayer.isSpectator()) { + ++this.activePlayers; +- if (entityplayer.isSleeping() || entityplayer.fauxSleeping) { // CraftBukkit ++ if ((entityplayer.isSleeping() || entityplayer.fauxSleeping) || (entityplayer.level.purpurConfig.idleTimeoutCountAsSleeping && entityplayer.isAfk())) { // CraftBukkit // Purpur + ++this.sleepingPlayers; + } + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/stats/ServerRecipeBook.java b/src/main/java/net/minecraft/stats/ServerRecipeBook.java +index d13ed3069e944d138442ea440ac3eaf8d44c18d3..29ac7f202aa23f7e6fcdc9829af3d59875c92d4e 100644 +--- a/src/main/java/net/minecraft/stats/ServerRecipeBook.java ++++ b/src/main/java/net/minecraft/stats/ServerRecipeBook.java +@@ -122,6 +122,7 @@ public class ServerRecipeBook extends RecipeBook { + Optional> optional = recipeManager.byKey(minecraftkey); + + if (!optional.isPresent()) { ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressUnrecognizedRecipeErrors) // Purpur + ServerRecipeBook.LOGGER.error("Tried to load unrecognized recipe: {} removed now.", minecraftkey); + } else { + handler.accept((Recipe) optional.get()); +diff --git a/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java b/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java +index 196c7331138fee2822c76aacd136f9da040e0049..c6c30d99399c5cde2b0ec2f320d81d952b422d78 100644 +--- a/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java ++++ b/src/main/java/net/minecraft/util/profiling/ActiveProfiler.java +@@ -55,7 +55,7 @@ public class ActiveProfiler implements ProfileCollector { + this.started = true; + this.path = ""; + this.paths.clear(); +- this.push("root"); ++ //this.push("root"); // Purpur + } + } + +@@ -64,7 +64,7 @@ public class ActiveProfiler implements ProfileCollector { + if (!this.started) { + LOGGER.error("Profiler tick already ended - missing startTick()?"); + } else { +- this.pop(); ++ //this.pop(); // Purpur + this.started = false; + if (!this.path.isEmpty()) { + LOGGER.error("Profiler tick ended before path was fully popped (remainder: '{}'). Mismatched push/pop?", LogUtils.defer(() -> { +@@ -93,7 +93,7 @@ public class ActiveProfiler implements ProfileCollector { + + @Override + public void push(Supplier locationGetter) { +- this.push(locationGetter.get()); ++ //this.push(locationGetter.get()); // Purpur + } + + @Override +@@ -132,14 +132,14 @@ public class ActiveProfiler implements ProfileCollector { + + @Override + public void popPush(String location) { +- this.pop(); +- this.push(location); ++ //this.pop(); // Purpur ++ //this.push(location); // Purpur + } + + @Override + public void popPush(Supplier locationGetter) { +- this.pop(); +- this.push(locationGetter); ++ //this.pop(); // Purpur ++ //this.push(locationGetter); // Purpur + } + + private ActiveProfiler.PathEntry getCurrentEntry() { +diff --git a/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java b/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java +index 2e6e8eac987c4ef6b2dcd3de592d8a51d2b29792..863343a87fe34d72f04af89d75268b477b2adc7a 100644 +--- a/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java ++++ b/src/main/java/net/minecraft/util/profiling/ProfilerFiller.java +@@ -6,32 +6,44 @@ import net.minecraft.util.profiling.metrics.MetricCategory; + public interface ProfilerFiller { + String ROOT = "root"; + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void startTick(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void endTick(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void push(String location); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void push(Supplier locationGetter); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void pop(); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void popPush(String location); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void popPush(Supplier locationGetter); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void markForCharting(MetricCategory type); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + default void incrementCounter(String marker) { +- this.incrementCounter(marker, 1); ++ //this.incrementCounter(marker, 1); // Purpur + } + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void incrementCounter(String marker, int num); + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + default void incrementCounter(Supplier markerGetter) { +- this.incrementCounter(markerGetter, 1); ++ //this.incrementCounter(markerGetter, 1); // Purpur + } + ++ @io.papermc.paper.annotation.DoNotUse // Purpur + void incrementCounter(Supplier markerGetter, int num); + + static ProfilerFiller tee(final ProfilerFiller a, final ProfilerFiller b) { +@@ -41,62 +53,62 @@ public interface ProfilerFiller { + return b == InactiveProfiler.INSTANCE ? a : new ProfilerFiller() { + @Override + public void startTick() { +- a.startTick(); +- b.startTick(); ++ //a.startTick(); // Purpur ++ //b.startTick(); // Purpur + } + + @Override + public void endTick() { +- a.endTick(); +- b.endTick(); ++ //a.endTick(); // Purpur ++ //b.endTick(); // Purpur + } + + @Override + public void push(String location) { +- a.push(location); +- b.push(location); ++ //a.push(location); // Purpur ++ //b.push(location); // Purpur + } + + @Override + public void push(Supplier locationGetter) { +- a.push(locationGetter); +- b.push(locationGetter); ++ //a.push(locationGetter); // Purpur ++ //b.push(locationGetter); // Purpur + } + + @Override + public void markForCharting(MetricCategory type) { +- a.markForCharting(type); +- b.markForCharting(type); ++ //a.markForCharting(type); // Purpur ++ //b.markForCharting(type); // Purpur + } + + @Override + public void pop() { +- a.pop(); +- b.pop(); ++ //a.pop(); // Purpur ++ //b.pop(); // Purpur + } + + @Override + public void popPush(String location) { +- a.popPush(location); +- b.popPush(location); ++ //a.popPush(location); // Purpur ++ //b.popPush(location); // Purpur + } + + @Override + public void popPush(Supplier locationGetter) { +- a.popPush(locationGetter); +- b.popPush(locationGetter); ++ //a.popPush(locationGetter); // Purpur ++ //b.popPush(locationGetter); // Purpur + } + + @Override + public void incrementCounter(String marker, int num) { +- a.incrementCounter(marker, num); +- b.incrementCounter(marker, num); ++ //a.incrementCounter(marker, num); // Purpur ++ //b.incrementCounter(marker, num); // Purpur + } + + @Override + public void incrementCounter(Supplier markerGetter, int num) { +- a.incrementCounter(markerGetter, num); +- b.incrementCounter(markerGetter, num); ++ //a.incrementCounter(markerGetter, num); // Purpur ++ //b.incrementCounter(markerGetter, num); // Purpur + } + }; + } +diff --git a/src/main/java/net/minecraft/world/damagesource/CombatRules.java b/src/main/java/net/minecraft/world/damagesource/CombatRules.java +index ccbfcef3e83b1bef364447657bfd08a92d615cf6..aa2331c6df4e79d4bb0add071a0b11d2a3a08b88 100644 +--- a/src/main/java/net/minecraft/world/damagesource/CombatRules.java ++++ b/src/main/java/net/minecraft/world/damagesource/CombatRules.java +@@ -11,12 +11,12 @@ public class CombatRules { + + public static float getDamageAfterAbsorb(float damage, float armor, float armorToughness) { + float f = 2.0F + armorToughness / 4.0F; +- float g = Mth.clamp(armor - damage / f, armor * 0.2F, 20.0F); ++ float g = Mth.clamp(armor - damage / f, armor * 0.2F, org.purpurmc.purpur.PurpurConfig.limitArmor ? 20F : Float.MAX_VALUE); // Purpur + return damage * (1.0F - g / 25.0F); + } + + public static float getDamageAfterMagicAbsorb(float damageDealt, float protection) { +- float f = Mth.clamp(protection, 0.0F, 20.0F); ++ float f = Mth.clamp(protection, 0.0F, org.purpurmc.purpur.PurpurConfig.limitArmor ? 20F : Float.MAX_VALUE); // Purpur + return damageDealt * (1.0F - f / 25.0F); + } + } +diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +index 93a1e990b0a6caae4143c2f9d09bfb368fa1d6db..615611fe372d6edaef56db058bbf2cf7641e3c26 100644 +--- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java ++++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +@@ -126,6 +126,15 @@ public class DamageSource { + } + } + ++ // Purpur start ++ public Component getLocalizedDeathMessage(String str, LivingEntity entity) { ++ net.kyori.adventure.text.Component name = io.papermc.paper.adventure.PaperAdventure.asAdventure(entity.getDisplayName()); ++ net.kyori.adventure.text.minimessage.tag.resolver.TagResolver template = net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("player", name); ++ net.kyori.adventure.text.Component component = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(str, template); ++ return io.papermc.paper.adventure.PaperAdventure.asVanilla(component); ++ } ++ // Purpur end ++ + public String getMsgId() { + return this.type().msgId(); + } +diff --git a/src/main/java/net/minecraft/world/effect/MobEffect.java b/src/main/java/net/minecraft/world/effect/MobEffect.java +index 2cc714585fc3790b70a7ad1ab8034543462e2b3b..22d7f04cefafa0115a4504e37380787777091b18 100644 +--- a/src/main/java/net/minecraft/world/effect/MobEffect.java ++++ b/src/main/java/net/minecraft/world/effect/MobEffect.java +@@ -60,16 +60,16 @@ public class MobEffect { + public void applyEffectTick(LivingEntity entity, int amplifier) { + if (this == MobEffects.REGENERATION) { + if (entity.getHealth() < entity.getMaxHealth()) { +- entity.heal(1.0F, RegainReason.MAGIC_REGEN); // CraftBukkit ++ entity.heal(entity.level.purpurConfig.entityHealthRegenAmount, RegainReason.MAGIC_REGEN); // CraftBukkit // Purpur + } + } else if (this == MobEffects.POISON) { +- if (entity.getHealth() > 1.0F) { +- entity.hurt(entity.damageSources().poison, 1.0F); // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON ++ if (entity.getHealth() > entity.level.purpurConfig.entityMinimalHealthPoison) { // Purpur ++ entity.hurt(entity.damageSources().poison, entity.level.purpurConfig.entityPoisonDegenerationAmount); // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON // Purpur + } + } else if (this == MobEffects.WITHER) { +- entity.hurt(entity.damageSources().wither(), 1.0F); ++ entity.hurt(entity.damageSources().wither(), entity.level.purpurConfig.entityWitherDegenerationAmount); // Purpur + } else if (this == MobEffects.HUNGER && entity instanceof Player) { +- ((Player) entity).causeFoodExhaustion(0.005F * (float) (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent ++ ((Player) entity).causeFoodExhaustion(entity.level.purpurConfig.humanHungerExhaustionAmount * (float) (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent // Purpur + } else if (this == MobEffects.SATURATION && entity instanceof Player) { + if (!entity.level.isClientSide) { + // CraftBukkit start +@@ -79,7 +79,7 @@ public class MobEffect { + org.bukkit.event.entity.FoodLevelChangeEvent event = CraftEventFactory.callFoodLevelChangeEvent(entityhuman, amplifier + 1 + oldFoodLevel); + + if (!event.isCancelled()) { +- entityhuman.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, 1.0F); ++ entityhuman.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, entity.level.purpurConfig.humanSaturationRegenAmount); // Purpur + } + + ((ServerPlayer) entityhuman).connection.send(new ClientboundSetHealthPacket(((ServerPlayer) entityhuman).getBukkitEntity().getScaledHealth(), entityhuman.getFoodData().foodLevel, entityhuman.getFoodData().saturationLevel)); +diff --git a/src/main/java/net/minecraft/world/effect/MobEffectInstance.java b/src/main/java/net/minecraft/world/effect/MobEffectInstance.java +index 14fab63346d56c72cd7534a04760efd10eef4295..745e792482f61c571e2efbd4200dd1bdaef6e474 100644 +--- a/src/main/java/net/minecraft/world/effect/MobEffectInstance.java ++++ b/src/main/java/net/minecraft/world/effect/MobEffectInstance.java +@@ -14,6 +14,7 @@ import net.minecraft.util.ExtraCodecs; + import net.minecraft.util.Mth; + import net.minecraft.world.entity.LivingEntity; + import org.slf4j.Logger; ++import org.bukkit.NamespacedKey; + + public class MobEffectInstance implements Comparable { + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -25,6 +26,7 @@ public class MobEffectInstance implements Comparable { + private boolean visible; + private boolean showIcon; + @Nullable ++ private NamespacedKey key; // Purpur - add key + private MobEffectInstance hiddenEffect; + private final Optional factorData; + +@@ -44,17 +46,36 @@ public class MobEffectInstance implements Comparable { + this(type, duration, amplifier, ambient, visible, visible); + } + ++ // Purpur start ++ public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean visible, @Nullable NamespacedKey key) { ++ this(type, duration, amplifier, ambient, visible, visible, key); ++ } ++ // Purpur end ++ + public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon) { +- this(type, duration, amplifier, ambient, showParticles, showIcon, (MobEffectInstance)null, type.createFactorData()); ++ // Purpur start ++ this(type, duration, amplifier, ambient, showParticles, showIcon, (MobEffectInstance)null, type.createFactorData(), (NamespacedKey)null); ++ } ++ ++ public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon, @Nullable NamespacedKey key) { ++ this(type, duration, amplifier, ambient, showParticles, showIcon, (MobEffectInstance)null, type.createFactorData(), key); ++ // Purpur end + } + + public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon, @Nullable MobEffectInstance hiddenEffect, Optional factorCalculationData) { ++ // Purpur start ++ this(type, duration, amplifier, ambient, showParticles, showIcon, hiddenEffect, factorCalculationData, (NamespacedKey) null); ++ } ++ ++ public MobEffectInstance(MobEffect type, int duration, int amplifier, boolean ambient, boolean showParticles, boolean showIcon, @Nullable MobEffectInstance hiddenEffect, Optional factorCalculationData, @Nullable NamespacedKey key) { ++ // Purpur end + this.effect = type; + this.duration = duration; + this.amplifier = amplifier; + this.ambient = ambient; + this.visible = showParticles; + this.showIcon = showIcon; ++ this.key = key; // Purpur - add key + this.hiddenEffect = hiddenEffect; + this.factorData = factorCalculationData; + } +@@ -75,6 +96,7 @@ public class MobEffectInstance implements Comparable { + this.ambient = that.ambient; + this.visible = that.visible; + this.showIcon = that.showIcon; ++ this.key = that.key; // Purpur - add key + } + + public boolean update(MobEffectInstance that) { +@@ -120,6 +142,13 @@ public class MobEffectInstance implements Comparable { + bl = true; + } + ++ // Purpur start ++ if (that.key != this.key) { ++ this.key = that.key; ++ bl = true; ++ } ++ // Purpur end ++ + return bl; + } + +@@ -163,6 +192,17 @@ public class MobEffectInstance implements Comparable { + return this.showIcon; + } + ++ // Purpur start ++ public boolean hasKey() { ++ return this.key != null; ++ } ++ ++ @Nullable ++ public NamespacedKey getKey() { ++ return this.key; ++ } ++ // Purpur end ++ + public boolean tick(LivingEntity entity, Runnable overwriteCallback) { + if (this.hasRemainingDuration()) { + int i = this.isInfiniteDuration() ? entity.tickCount : this.duration; +@@ -226,6 +266,12 @@ public class MobEffectInstance implements Comparable { + string = string + ", Show Icon: false"; + } + ++ // Purpur start ++ if (this.hasKey()) { ++ string = string + ", Key: " + this.key; ++ } ++ // Purpur end ++ + return string; + } + +@@ -241,7 +287,7 @@ public class MobEffectInstance implements Comparable { + return false; + } else { + MobEffectInstance mobEffectInstance = (MobEffectInstance)object; +- return this.duration == mobEffectInstance.duration && this.amplifier == mobEffectInstance.amplifier && this.ambient == mobEffectInstance.ambient && this.effect.equals(mobEffectInstance.effect); ++ return this.duration == mobEffectInstance.duration && this.amplifier == mobEffectInstance.amplifier && this.ambient == mobEffectInstance.ambient && this.effect.equals(mobEffectInstance.effect) && this.key == mobEffectInstance.key; // Purpur - add key + } + } + +@@ -265,6 +311,11 @@ public class MobEffectInstance implements Comparable { + nbt.putBoolean("Ambient", this.isAmbient()); + nbt.putBoolean("ShowParticles", this.isVisible()); + nbt.putBoolean("ShowIcon", this.showIcon()); ++ // Purpur start ++ if (this.key != null) { ++ nbt.putString("Key", this.key.toString()); ++ } ++ // Purpur end + if (this.hiddenEffect != null) { + CompoundTag compoundTag = new CompoundTag(); + this.hiddenEffect.save(compoundTag); +@@ -299,6 +350,13 @@ public class MobEffectInstance implements Comparable { + bl3 = nbt.getBoolean("ShowIcon"); + } + ++ // Purpur start ++ NamespacedKey key = null; ++ if (nbt.contains("Key")) { ++ key = NamespacedKey.fromString(nbt.getString("Key")); ++ } ++ // Purpur end ++ + MobEffectInstance mobEffectInstance = null; + if (nbt.contains("HiddenEffect", 10)) { + mobEffectInstance = loadSpecifiedEffect(type, nbt.getCompound("HiddenEffect")); +@@ -311,7 +369,7 @@ public class MobEffectInstance implements Comparable { + optional = Optional.empty(); + } + +- return new MobEffectInstance(type, j, Math.max(i, 0), bl, bl2, bl3, mobEffectInstance, optional); ++ return new MobEffectInstance(type, j, Math.max(i, 0), bl, bl2, bl3, mobEffectInstance, optional, key); // Purpur - add key + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 1030b54428b15f387580a2ce47a7a1eb0c8d521b..f31f4ecf8fb07f6cf01eea0aa14f5d454a4ce2a3 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -156,7 +156,7 @@ import org.bukkit.plugin.PluginManager; + // CraftBukkit end + + public abstract class Entity implements Nameable, EntityAccess, CommandSource { +- ++ public static javax.script.ScriptEngine scriptEngine = new javax.script.ScriptEngineManager().getEngineByName("rhino"); // Purpur + // CraftBukkit start + private static final int CURRENT_LEVEL = 2; + public boolean preserveMotion = true; // Paper - keep initial motion on first setPositionRotation +@@ -320,7 +320,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + public double xOld; + public double yOld; + public double zOld; +- private float maxUpStep; ++ public float maxUpStep; // Purpur - private -> public + public boolean noPhysics; + protected final RandomSource random; + public int tickCount; +@@ -362,7 +362,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + private final Set tags; + private final double[] pistonDeltas; + private long pistonDeltasGameTime; +- private EntityDimensions dimensions; ++ protected EntityDimensions dimensions; // Purpur - private -> protected + private float eyeHeight; + public boolean isInPowderSnow; + public boolean wasInPowderSnow; +@@ -400,6 +400,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + private UUID originWorld; + public boolean freezeLocked = false; // Paper - Freeze Tick Lock API + public boolean collidingWithWorldBorder; // Paper ++ public @Nullable Boolean immuneToFire = null; // Purpur - Fire immune API + + public void setOrigin(@javax.annotation.Nonnull Location location) { + this.origin = location.toVector(); +@@ -579,7 +580,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.bb = Entity.INITIAL_AABB; + this.stuckSpeedMultiplier = Vec3.ZERO; + this.nextStep = 1.0F; +- this.random = SHARED_RANDOM; // Paper ++ this.random = world == null || world.purpurConfig.entitySharedRandom ? SHARED_RANDOM : RandomSource.create(); // Paper // Purpur + this.remainingFireTicks = -this.getFireImmuneTicks(); + this.fluidHeight = new Object2DoubleArrayMap(2); + this.fluidOnEyes = new HashSet(); +@@ -824,7 +825,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return; + } + // Pufferfish end - entity TTL +- this.level.getProfiler().push("entityBaseTick"); ++ //this.level.getProfiler().push("entityBaseTick"); // Purpur + if (firstTick && this instanceof net.minecraft.world.entity.NeutralMob neutralMob) neutralMob.tickInitialPersistentAnger(level); // Paper - Update last hurt when ticking + this.feetBlockState = null; + if (this.isPassenger() && this.getVehicle().isRemoved()) { +@@ -885,7 +886,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + this.firstTick = false; +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + public void setSharedFlagOnFire(boolean onFire) { +@@ -894,10 +895,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + public void checkOutOfWorld() { + // Paper start - Configurable nether ceiling damage +- if (this.getY() < (double) (this.level.getMinBuildHeight() - 64) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER ++ if (this.getY() < (double) (this.level.getMinBuildHeight() + level.purpurConfig.voidDamageHeight) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER // Purpur + && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> this.getY() >= v) + && (!(this instanceof Player player) || !player.getAbilities().invulnerable))) { + // Paper end ++ if (this.level.purpurConfig.teleportOnNetherCeilingDamage && this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER && this instanceof ServerPlayer player) player.teleport(MCUtil.toLocation(this.level, this.level.getSharedSpawnPos())); else // Purpur + this.outOfWorld(); + } + +@@ -1059,7 +1061,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + } + +- this.level.getProfiler().push("move"); ++ //this.level.getProfiler().push("move"); // Purpur + if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) { + movement = movement.multiply(this.stuckSpeedMultiplier); + this.stuckSpeedMultiplier = Vec3.ZERO; +@@ -1068,7 +1070,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + // Paper start - ignore movement changes while inactive. + if (isTemporarilyActive && !(this instanceof ItemEntity || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart) && movement == getDeltaMovement() && movementType == MoverType.SELF) { + setDeltaMovement(Vec3.ZERO); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + return; + } + // Paper end +@@ -1089,8 +1091,8 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.setPos(this.getX() + vec3d1.x, this.getY() + vec3d1.y, this.getZ() + vec3d1.z); + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("rest"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("rest"); // Purpur + boolean flag = !Mth.equal(movement.x, vec3d1.x); + boolean flag1 = !Mth.equal(movement.z, vec3d1.z); + +@@ -1109,7 +1111,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + this.checkFallDamage(vec3d1.y, this.onGround, iblockdata, blockposition); + if (this.isRemoved()) { +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } else { + if (this.horizontalCollision) { + Vec3 vec3d2 = this.getDeltaMovement(); +@@ -1250,7 +1252,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.setRemainingFireTicks(-this.getFireImmuneTicks()); + } + +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + } + // Paper start - detailed watchdog information +@@ -1677,7 +1679,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public boolean fireImmune() { +- return this.getType().fireImmune(); ++ return this.immuneToFire != null ? immuneToFire : this.getType().fireImmune(); // Purpur - add fire immune API + } + + public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { +@@ -1746,7 +1748,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return this.isInWater() || flag; + } + +- void updateInWaterStateAndDoWaterCurrentPushing() { ++ public void updateInWaterStateAndDoWaterCurrentPushing() { // Purpur - package-private -> public + Entity entity = this.getVehicle(); + + if (entity instanceof Boat) { +@@ -2339,6 +2341,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + nbt.putBoolean("Paper.FreezeLock", true); + } + // Paper end ++ // Purpur start ++ if (immuneToFire != null) { ++ nbt.putBoolean("Purpur.FireImmune", immuneToFire); ++ } ++ // Purpur end + return nbt; + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); +@@ -2507,6 +2514,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + freezeLocked = nbt.getBoolean("Paper.FreezeLock"); + } + // Paper end ++ // Purpur start ++ if (nbt.contains("Purpur.FireImmune")) { ++ immuneToFire = nbt.getBoolean("Purpur.FireImmune"); ++ } ++ // Purpur end + + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); +@@ -2823,6 +2835,13 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.passengers = ImmutableList.copyOf(list); + } + ++ // Purpur start ++ if (isRidable() && this.passengers.get(0) == entity && entity instanceof Player player) { ++ onMount(player); ++ this.rider = player; ++ } ++ // Purpur end ++ + this.gameEvent(GameEvent.ENTITY_MOUNT, entity); + } + return true; // CraftBukkit +@@ -2864,6 +2883,14 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return false; + } + // Spigot end ++ ++ // Purpur start ++ if (this.rider != null && this.passengers.get(0) == this.rider) { ++ onDismount(this.rider); ++ this.rider = null; ++ } ++ // Purpur end ++ + if (this.passengers.size() == 1 && this.passengers.get(0) == entity) { + this.passengers = ImmutableList.of(); + } else { +@@ -2923,12 +2950,15 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return Vec3.directionFromRotation(this.getRotationVector()); + } + ++ public BlockPos portalPos = BlockPos.ZERO; // Purpur + public void handleInsidePortal(BlockPos pos) { + if (this.isOnPortalCooldown()) { ++ if (!(level.purpurConfig.playerFixStuckPortal && this instanceof Player && !pos.equals(portalPos))) // Purpur + this.setPortalCooldown(); +- } else { ++ } else if (level.purpurConfig.entitiesCanUsePortals || this instanceof ServerPlayer) { // Purpur + if (!this.level.isClientSide && !pos.equals(this.portalEntrancePos)) { + this.portalEntrancePos = pos.immutable(); ++ portalPos = BlockPos.ZERO; // Purpur + } + + this.isInsidePortal = true; +@@ -2946,7 +2976,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + ServerLevel worldserver1 = minecraftserver.getLevel(resourcekey); + + if (true && !this.isPassenger() && this.portalTime++ >= i) { // CraftBukkit +- this.level.getProfiler().push("portal"); ++ //this.level.getProfiler().push("portal"); // Purpur + this.portalTime = i; + // Paper start + io.papermc.paper.event.entity.EntityPortalReadyEvent event = new io.papermc.paper.event.entity.EntityPortalReadyEvent(this.getBukkitEntity(), worldserver1 == null ? null : worldserver1.getWorld(), org.bukkit.PortalType.NETHER); +@@ -2964,7 +2994,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + } // Paper + // CraftBukkit end +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + this.isInsidePortal = false; +@@ -2979,7 +3009,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + this.processPortalCooldown(); +- this.tickEndPortal(); // Paper - make end portalling safe ++ if (this.level.purpurConfig.endPortalSafeTeleporting) this.tickEndPortal(); // Paper - make end portalling safe // Purpur + } + } + +@@ -3161,7 +3191,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public int getMaxAirSupply() { +- return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir() ++ return this.level == null? this.maxAirTicks : this.level.purpurConfig.drowningAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir() // Purpur + } + + public int getAirSupply() { +@@ -3431,14 +3461,14 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + // Paper end + if (this.level instanceof ServerLevel && !this.isRemoved()) { +- this.level.getProfiler().push("changeDimension"); ++ //this.level.getProfiler().push("changeDimension"); // Purpur + // CraftBukkit start + // this.decouple(); + if (worldserver == null) { + return null; + } + // CraftBukkit end +- this.level.getProfiler().push("reposition"); ++ //this.level.getProfiler().push("reposition"); // Purpur + PortalInfo shapedetectorshape = (location == null) ? this.findDimensionEntryPoint(worldserver) : new PortalInfo(new Vec3(location.x(), location.y(), location.z()), Vec3.ZERO, this.yRot, this.xRot, worldserver, null); // CraftBukkit + + if (shapedetectorshape == null) { +@@ -3472,7 +3502,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.unRide(); + // CraftBukkit end + +- this.level.getProfiler().popPush("reloading"); ++ //this.level.getProfiler().popPush("reloading"); // Purpur + // Paper start - Change lead drop timing to prevent dupe + if (this instanceof Mob) { + ((Mob) this).dropLeash(true, true); // Paper drop lead +@@ -3495,10 +3525,10 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + this.removeAfterChangingDimensions(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + ((ServerLevel) this.level).resetEmptyTime(); + worldserver.resetEmptyTime(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + return entity; + } + } else { +@@ -3618,7 +3648,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public boolean canChangeDimensions() { +- return !this.isPassenger() && !this.isVehicle() && isAlive() && valid; // Paper ++ return !this.isPassenger() && !this.isVehicle() && isAlive() && valid && (level.purpurConfig.entitiesCanUsePortals || this instanceof ServerPlayer); // Paper // Purpur + } + + public float getBlockExplosionResistance(Explosion explosion, BlockGetter world, BlockPos pos, BlockState blockState, FluidState fluidState, float max) { +@@ -3915,6 +3945,20 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return SlotAccess.NULL; + } + ++ // Purpur Start ++ public void sendMiniMessage(@Nullable String message) { ++ if (message != null && !message.isEmpty()) { ++ this.sendMessage(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(message)); ++ } ++ } ++ ++ public void sendMessage(@Nullable net.kyori.adventure.text.Component message) { ++ if (message != null) { ++ this.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message)); ++ } ++ } ++ // Purpur end ++ + @Override + public void sendSystemMessage(Component message) {} + +@@ -4196,6 +4240,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.yRotO = this.getYRot(); + } + ++ // Purpur start ++ public AABB getAxisForFluidCheck() { ++ return this.getBoundingBox().deflate(0.001D); ++ } ++ // Purpur end ++ + public boolean updateFluidHeightAndDoFluidPushing(TagKey tag, double speed) { + if (false && this.touchingUnloadedChunk()) { // Pufferfish - cost of a lookup here is the same cost as below, so skip + return false; +@@ -4724,4 +4774,64 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return ((net.minecraft.server.level.ServerChunkCache) level.getChunkSource()).isPositionTicking(this); + } + // Paper end ++ ++ // Purpur start ++ @Nullable ++ private Player rider = null; ++ ++ @Nullable ++ public Player getRider() { ++ return rider; ++ } ++ ++ public boolean isRidable() { ++ return false; ++ } ++ ++ public boolean isControllable() { ++ return true; ++ } ++ ++ public void onMount(Player rider) { ++ if (this instanceof Mob) { ++ ((Mob) this).setTarget(null, null, false); ++ ((Mob) this).getNavigation().stop(); ++ } ++ rider.setJumping(false); // fixes jump on mount ++ } ++ ++ public void onDismount(Player player) { ++ } ++ ++ public boolean onSpacebar() { ++ return false; ++ } ++ ++ public boolean onClick(InteractionHand hand) { ++ return false; ++ } ++ ++ public boolean processClick(InteractionHand hand) { ++ return false; ++ } ++ ++ public boolean canSaveToDisk() { ++ return true; ++ } ++ ++ // Purpur start - copied from Mob ++ public boolean isSunBurnTick() { ++ if (this.level.isDay() && !this.level.isClientSide) { ++ float f = this.getLightLevelDependentMagicValue(); ++ BlockPos blockposition = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); ++ boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; ++ ++ if (f > 0.5F && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && !flag && this.level.canSeeSky(blockposition)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/EntitySelector.java b/src/main/java/net/minecraft/world/entity/EntitySelector.java +index 72abebff2018cde2922e97ad6478f93da9aed3ec..412963d7af38a53b6010007278d959a5b11b83c3 100644 +--- a/src/main/java/net/minecraft/world/entity/EntitySelector.java ++++ b/src/main/java/net/minecraft/world/entity/EntitySelector.java +@@ -39,6 +39,7 @@ public final class EntitySelector { + return net.minecraft.util.Mth.clamp(serverPlayer.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= playerInsomniaTicks; + }; + // Paper end ++ public static Predicate notAfk = (player) -> !player.isAfk(); // Purpur + + private EntitySelector() {} + // Paper start +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index 8af0918d3a62de58a4b2af55022c812bb0e46092..3fc26a8976f4bfa28c2c6a862aac997d5f721f51 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -308,13 +308,24 @@ public class EntityType implements FeatureElement, EntityTypeT + private Component description; + @Nullable + private ResourceLocation lootTable; +- private final EntityDimensions dimensions; ++ private EntityDimensions dimensions; // Purpur - remove final ++ public void setDimensions(EntityDimensions dimensions) { this.dimensions = dimensions; } // Purpur + private final FeatureFlagSet requiredFeatures; + + private static EntityType register(String id, EntityType.Builder type) { // CraftBukkit - decompile error + return (EntityType) Registry.register(BuiltInRegistries.ENTITY_TYPE, id, (EntityType) type.build(id)); // CraftBukkit - decompile error + } + ++ // Purpur start ++ public static EntityType getFromBukkitType(org.bukkit.entity.EntityType bukkitType) { ++ return getFromKey(new ResourceLocation(bukkitType.getKey().toString())); ++ } ++ ++ public static EntityType getFromKey(ResourceLocation location) { ++ return BuiltInRegistries.ENTITY_TYPE.get(location); ++ } ++ // Purpur end ++ + public static ResourceLocation getKey(EntityType type) { + return BuiltInRegistries.ENTITY_TYPE.getKey(type); + } +@@ -530,6 +541,16 @@ public class EntityType implements FeatureElement, EntityTypeT + return this.category; + } + ++ // Purpur start ++ public String getName() { ++ return BuiltInRegistries.ENTITY_TYPE.getKey(this).getPath(); ++ } ++ ++ public String getTranslatedName() { ++ return getDescription().getString(); ++ } ++ // Purpur end ++ + public String getDescriptionId() { + if (this.descriptionId == null) { + this.descriptionId = Util.makeDescriptionId("entity", BuiltInRegistries.ENTITY_TYPE.getKey(this)); +@@ -591,6 +612,12 @@ public class EntityType implements FeatureElement, EntityTypeT + entity.load(nbt); + }, () -> { + EntityType.LOGGER.warn("Skipping Entity with id {}", nbt.getString("id")); ++ // Purpur start - log skipped entity's position ++ try { ++ ListTag pos = nbt.getList("Pos", 6); ++ EntityType.LOGGER.warn("Location: {} {},{},{}", world.getWorld().getName(), pos.getDouble(0), pos.getDouble(1), pos.getDouble(2)); ++ } catch (Throwable ignore) {} ++ // Purpur end + }); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ExperienceOrb.java b/src/main/java/net/minecraft/world/entity/ExperienceOrb.java +index a9f20e6a73e2e1875abd1e122a5d08c4ef44f9d8..0c0f422ef0c20477295cea0b6b3c4b2d0a7db265 100644 +--- a/src/main/java/net/minecraft/world/entity/ExperienceOrb.java ++++ b/src/main/java/net/minecraft/world/entity/ExperienceOrb.java +@@ -306,7 +306,7 @@ public class ExperienceOrb extends Entity { + public void playerTouch(Player player) { + if (!this.level.isClientSide) { + if (player.takeXpDelay == 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper +- player.takeXpDelay = CraftEventFactory.callPlayerXpCooldownEvent(player, 2, PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entityhuman.takeXpDelay = 2; ++ player.takeXpDelay = CraftEventFactory.callPlayerXpCooldownEvent(player, this.level.purpurConfig.playerExpPickupDelay, PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entityhuman.takeXpDelay = 2; // Purpur + player.take(this, 1); + int i = this.repairPlayerItems(player, this.value); + +@@ -324,7 +324,7 @@ public class ExperienceOrb extends Entity { + } + + private int repairPlayerItems(Player player, int amount) { +- Entry entry = EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player, ItemStack::isDamaged); ++ Entry entry = level.purpurConfig.useBetterMending ? EnchantmentHelper.getMostDamagedEquipment(Enchantments.MENDING, player) : EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player, ItemStack::isDamaged); // Purpur + + if (entry != null) { + ItemStack itemstack = (ItemStack) entry.getValue(); +diff --git a/src/main/java/net/minecraft/world/entity/GlowSquid.java b/src/main/java/net/minecraft/world/entity/GlowSquid.java +index c1e9b40a4a0f9cdc650caa88b5ea132e06ee2496..6f723171fa71d74b351b5cf0cd167bb6f7ca1691 100644 +--- a/src/main/java/net/minecraft/world/entity/GlowSquid.java ++++ b/src/main/java/net/minecraft/world/entity/GlowSquid.java +@@ -18,11 +18,45 @@ import net.minecraft.world.level.block.Blocks; + + public class GlowSquid extends Squid { + private static final EntityDataAccessor DATA_DARK_TICKS_REMAINING = SynchedEntityData.defineId(GlowSquid.class, EntityDataSerializers.INT); ++ private static final net.minecraft.network.syncher.EntityDataAccessor SQUID_COLOR = net.minecraft.network.syncher.SynchedEntityData.defineId(GlowSquid.class, net.minecraft.network.syncher.EntityDataSerializers.STRING); // Purpur + + public GlowSquid(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.glowSquidRidable; ++ } ++ ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.glowSquidControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.glowSquidMaxHealth); ++ } ++ ++ @Override ++ public boolean canFly() { ++ return this.level.purpurConfig.glowSquidsCanFly; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.glowSquidTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.glowSquidAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected ParticleOptions getInkParticle() { + return ParticleTypes.GLOW_SQUID_INK; +@@ -32,6 +66,7 @@ public class GlowSquid extends Squid { + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(DATA_DARK_TICKS_REMAINING, 0); ++ this.entityData.define(SQUID_COLOR, this.level.purpurConfig.glowSquidColorMode.getRandom(this.random).toString()); // Purpur + } + + @Override +@@ -58,12 +93,14 @@ public class GlowSquid extends Squid { + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("DarkTicksRemaining", this.getDarkTicksRemaining()); ++ nbt.putString("Colour", this.entityData.get(SQUID_COLOR)); // Purpur - key must match rainglow + } + + @Override + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.setDarkTicks(nbt.getInt("DarkTicksRemaining")); ++ if (nbt.contains("Colour")) this.entityData.set(SQUID_COLOR, nbt.getString("Colour")); // Purpur - key must match rainglow + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index f3d96caa83ef4a8083b78e3265282d4723e37d28..b67660cda74a4754d1701e746aca99bde868c150 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -216,9 +216,9 @@ public abstract class LivingEntity extends Entity implements Attackable { + protected int deathScore; + public float lastHurt; + public boolean jumping; +- public float xxa; +- public float yya; +- public float zza; ++ public float xxa; public float getStrafeMot() { return xxa; } public void setStrafeMot(float strafe) { xxa = strafe; } // Purpur - OBFHELPER ++ public float yya; public float getVerticalMot() { return yya; } public void setVerticalMot(float vertical) { yya = vertical; } // Purpur - OBFHELPER ++ public float zza; public float getForwardMot() { return zza; } public void setForwardMot(float forward) { zza = forward; } // Purpur - OBFHELPER + protected int lerpSteps; + protected double lerpX; + protected double lerpY; +@@ -251,6 +251,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + private boolean skipDropExperience; + // CraftBukkit start + public int expToDrop; ++ public float safeFallDistance = 3.0F; // Purpur + public boolean forceDrops; + public ArrayList drops = new ArrayList(); + public final org.bukkit.craftbukkit.attribute.CraftAttributeMap craftAttributes; +@@ -260,6 +261,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper + public boolean silentDeath = false; // Paper - mark entity as dying silently for cancellable death event + public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper ++ protected boolean shouldBurnInDay = false; public boolean shouldBurnInDay() { return this.shouldBurnInDay; } public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } // Purpur + + @Override + public float getBukkitYaw() { +@@ -284,7 +286,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.effectsDirty = true; + this.useItem = ItemStack.EMPTY; + this.lastClimbablePos = Optional.empty(); +- this.attributes = new AttributeMap(DefaultAttributes.getSupplier(type)); ++ this.attributes = new AttributeMap(DefaultAttributes.getSupplier(type), this); // Purpur ++ this.initAttributes(); // Purpur + this.craftAttributes = new CraftAttributeMap(this.attributes); // CraftBukkit + // CraftBukkit - setHealth(getMaxHealth()) inlined and simplified to skip the instanceof check for EntityPlayer, as getBukkitEntity() is not initialized in constructor + this.entityData.set(LivingEntity.DATA_HEALTH_ID, (float) this.getAttribute(Attributes.MAX_HEALTH).getValue()); +@@ -300,6 +303,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.brain = this.makeBrain(new Dynamic(dynamicopsnbt, (Tag) dynamicopsnbt.createMap((Map) ImmutableMap.of(dynamicopsnbt.createString("memories"), (Tag) dynamicopsnbt.emptyMap())))); + } + ++ protected void initAttributes() {}// Purpur ++ + public Brain getBrain() { + return this.brain; + } +@@ -335,6 +340,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + public static AttributeSupplier.Builder createLivingAttributes() { + return AttributeSupplier.builder().add(Attributes.MAX_HEALTH).add(Attributes.KNOCKBACK_RESISTANCE).add(Attributes.MOVEMENT_SPEED).add(Attributes.ARMOR).add(Attributes.ARMOR_TOUGHNESS); + } ++ public boolean shouldSendAttribute(Attribute attribute) { return true; } // Purpur + + @Override + protected void checkFallDamage(double heightDifference, boolean onGround, BlockState state, BlockPos landedPosition) { +@@ -347,8 +353,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.tryAddSoulSpeed(); + } + +- if (!this.level.isClientSide && this.fallDistance > 3.0F && onGround) { +- float f = (float) Mth.ceil(this.fallDistance - 3.0F); ++ if (!this.level.isClientSide && this.fallDistance > this.safeFallDistance && onGround) { // Purpur ++ float f = (float) Mth.ceil(this.fallDistance - this.safeFallDistance); // Purpur + + if (!state.isAir()) { + double d1 = Math.min((double) (0.2F + f / 15.0F), 2.5D); +@@ -387,7 +393,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + super.baseTick(); +- this.level.getProfiler().push("livingEntityBaseTick"); ++ //this.level.getProfiler().push("livingEntityBaseTick"); // Purpur + if (this.fireImmune() || this.level.isClientSide) { + this.clearFire(); + } +@@ -405,6 +411,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + double d1 = this.level.getWorldBorder().getDamagePerBlock(); + + if (d1 > 0.0D) { ++ if (level.purpurConfig.teleportIfOutsideBorder && this instanceof ServerPlayer) { ((ServerPlayer) this).teleport(io.papermc.paper.util.MCUtil.toLocation(level, ((ServerLevel) level).getSharedSpawnPos())); return; } // Purpur + this.hurt(this.damageSources().inWall(), (float) Math.max(1, Mth.floor(-d0 * d1))); + } + } +@@ -416,7 +423,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (flag1) { + this.setAirSupply(this.decreaseAirSupply(this.getAirSupply())); +- if (this.getAirSupply() == -20) { ++ if (this.getAirSupply() == -this.level.purpurConfig.drowningDamageInterval) { // Purpur + this.setAirSupply(0); + Vec3 vec3d = this.getDeltaMovement(); + +@@ -428,7 +435,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.level.addParticle(ParticleTypes.BUBBLE, this.getX() + d2, this.getY() + d3, this.getZ() + d4, vec3d.x, vec3d.y, vec3d.z); + } + +- this.hurt(this.damageSources().drown(), 2.0F); ++ this.hurt(this.damageSources().drown(), (float) this.level.purpurConfig.damageFromDrowning); // Purpur + } + } + +@@ -489,7 +496,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.yHeadRotO = this.yHeadRot; + this.yRotO = this.getYRot(); + this.xRotO = this.getXRot(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + public boolean canSpawnSoulSpeedParticle() { +@@ -781,6 +788,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> { + nbt.put("Brain", nbtbase); + }); ++ nbt.putBoolean("Purpur.ShouldBurnInDay", shouldBurnInDay); // Purpur + } + + @Override +@@ -865,6 +873,11 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.brain = this.makeBrain(new Dynamic(NbtOps.INSTANCE, nbt.get("Brain"))); + } + ++ // Purpur start ++ if (nbt.contains("Purpur.ShouldBurnInDay")) { ++ shouldBurnInDay = nbt.getBoolean("Purpur.ShouldBurnInDay"); ++ } ++ // Purpur end + } + + // CraftBukkit start +@@ -1010,9 +1023,31 @@ public abstract class LivingEntity extends Entity implements Attackable { + ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); + EntityType entitytypes = entity.getType(); + +- if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL) || entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD) || entitytypes == EntityType.PIGLIN && itemstack.is(Items.PIGLIN_HEAD) || entitytypes == EntityType.PIGLIN_BRUTE && itemstack.is(Items.PIGLIN_HEAD) || entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { +- d0 *= 0.5D; ++ // Purpur start ++ if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL)) { ++ d0 *= entity.level.purpurConfig.skeletonHeadVisibilityPercent; ++ } ++ else if (entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD)) { ++ d0 *= entity.level.purpurConfig.zombieHeadVisibilityPercent; ++ } ++ else if (entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { ++ d0 *= entity.level.purpurConfig.creeperHeadVisibilityPercent; ++ } ++ else if ((entitytypes == EntityType.PIGLIN || entitytypes == EntityType.PIGLIN_BRUTE) && itemstack.is(Items.PIGLIN_HEAD)) { ++ d0 *= entity.level.purpurConfig.piglinHeadVisibilityPercent; ++ } ++ // Purpur end ++ ++ // Purpur start ++ if (entity instanceof LivingEntity entityliving) { ++ if (entityliving.hasEffect(MobEffects.BLINDNESS)) { ++ int amplifier = entityliving.getEffect(MobEffects.BLINDNESS).getAmplifier(); ++ for (int i = 0; i < amplifier; i++) { ++ d0 *= this.level.purpurConfig.mobsBlindnessMultiplier; ++ } ++ } + } ++ // Purpur end + } + + return d0; +@@ -1072,6 +1107,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + for (flag = false; iterator.hasNext(); flag = true) { + // CraftBukkit start + MobEffectInstance effect = (MobEffectInstance) iterator.next(); ++ if (cause == EntityPotionEffectEvent.Cause.MILK && !this.level.purpurConfig.milkClearsBeneficialEffects && effect.getEffect().isBeneficial()) continue; // Purpur + EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effect, null, cause, EntityPotionEffectEvent.Action.CLEARED); + if (event.isCancelled()) { + continue; +@@ -1428,13 +1464,13 @@ public abstract class LivingEntity extends Entity implements Attackable { + if (entity1 instanceof net.minecraft.world.entity.player.Player) { + net.minecraft.world.entity.player.Player entityhuman = (net.minecraft.world.entity.player.Player) entity1; + +- this.lastHurtByPlayerTime = 100; ++ this.lastHurtByPlayerTime = this.level.purpurConfig.mobLastHurtByPlayerTime; // Purpur + this.lastHurtByPlayer = entityhuman; + } else if (entity1 instanceof Wolf) { + Wolf entitywolf = (Wolf) entity1; + + if (entitywolf.isTame()) { +- this.lastHurtByPlayerTime = 100; ++ this.lastHurtByPlayerTime = this.level.purpurConfig.mobLastHurtByPlayerTime; // Purpur + LivingEntity entityliving2 = entitywolf.getOwner(); + + if (entityliving2 instanceof net.minecraft.world.entity.player.Player) { +@@ -1545,6 +1581,18 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + } + ++ // Purpur start ++ if (level.purpurConfig.totemOfUndyingWorksInInventory && this instanceof ServerPlayer player && (itemstack == null || itemstack.getItem() != Items.TOTEM_OF_UNDYING) && player.getBukkitEntity().hasPermission("purpur.inventory_totem")) { ++ for (ItemStack item : player.getInventory().items) { ++ if (item.getItem() == Items.TOTEM_OF_UNDYING) { ++ itemstack1 = item; ++ itemstack = item.copy(); ++ break; ++ } ++ } ++ } ++ // Purpur end ++ + org.bukkit.inventory.EquipmentSlot handSlot = (hand != null) ? org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand) : null; + EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot); + event.setCancelled(itemstack == null); +@@ -1705,7 +1753,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + boolean flag = false; + + if (this.dead && adversary instanceof WitherBoss) { // Paper +- if (this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.level.purpurConfig.witherBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + BlockPos blockposition = this.blockPosition(); + BlockState iblockdata = Blocks.WITHER_ROSE.defaultBlockState(); + +@@ -1751,6 +1799,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + this.dropEquipment(); // CraftBukkit - from below + if (this.shouldDropLoot() && this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { ++ if (!(source.is(net.minecraft.world.damagesource.DamageTypes.CRAMMING) && level.purpurConfig.disableDropsOnCrammingDeath)) { // Purpur + this.dropFromLootTable(source, flag); + // Paper start + final boolean prev = this.clearEquipmentSlots; +@@ -1759,6 +1808,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + // Paper end + this.dropCustomDeathLoot(source, i, flag); + this.clearEquipmentSlots = prev; // Paper ++ } // Purpur + } + // CraftBukkit start - Call death event // Paper start - call advancement triggers with correct entity equipment + org.bukkit.event.entity.EntityDeathEvent deathEvent = CraftEventFactory.callEntityDeathEvent(this, this.drops, () -> { +@@ -2014,7 +2064,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + MobEffectInstance mobeffect = this.getEffect(MobEffects.JUMP); + float f2 = mobeffect == null ? 0.0F : (float) (mobeffect.getAmplifier() + 1); + +- return Mth.ceil((fallDistance - 3.0F - f2) * damageMultiplier); ++ return Mth.ceil((fallDistance - this.safeFallDistance - f2) * damageMultiplier); // Purpur + } + } + +@@ -2237,6 +2287,20 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + } + ++ // Purpur start ++ if (damagesource.getEntity() instanceof net.minecraft.world.entity.player.Player player && damagesource.getEntity().level.purpurConfig.creativeOnePunch) { ++ if (player.isCreative()) { ++ double attackDamage = 0; ++ for (AttributeModifier modifier : player.getMainHandItem().getAttributeModifiers(EquipmentSlot.MAINHAND).get(Attributes.ATTACK_DAMAGE)) { ++ attackDamage += modifier.getAmount(); ++ } ++ if (attackDamage == 0) { ++ this.setHealth(0); ++ } ++ } ++ } ++ // Purpur end ++ + if (f > 0 || !human) { + if (human) { + // PAIL: Be sure to drag all this code from the EntityHuman subclass each update. +@@ -2455,7 +2519,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + @Override + protected void outOfWorld() { +- this.hurt(this.damageSources().outOfWorld(), 4.0F); ++ this.hurt(this.damageSources().outOfWorld(), (float) level.purpurConfig.voidDamageDealt); // Purpur + } + + protected void updateSwingTime() { +@@ -2652,7 +2716,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + protected long lastJumpTime = 0L; // Paper +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + double d0 = (double) this.getJumpPower() + this.getJumpBoostPower(); + Vec3 vec3d = this.getDeltaMovement(); + // Paper start +@@ -2806,6 +2870,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (f3 > 0.0F) { + this.playSound(this.getFallDamageSound((int) f3), 1.0F, 1.0F); ++ if (level.purpurConfig.elytraKineticDamage) // Purpur + this.hurt(this.damageSources().flyIntoWall(), f3); + } + } +@@ -3028,10 +3093,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.run += (f3 - this.run) * 0.3F; +- this.level.getProfiler().push("headTurn"); ++ //this.level.getProfiler().push("headTurn"); // Purpur + f2 = this.tickHeadTurn(f1, f2); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("rangeChecks"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("rangeChecks"); // Purpur + + // Paper start - stop large pitch and yaw changes from crashing the server + this.yRotO += Math.round((this.getYRot() - this.yRotO) / 360.0F) * 360.0F; +@@ -3043,7 +3108,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.yHeadRotO += Math.round((this.yHeadRot - this.yHeadRotO) / 360.0F) * 360.0F; + // Paper end + +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + this.animStep += f2; + if (this.isFallFlying()) { + ++this.fallFlyTicks; +@@ -3332,19 +3397,19 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.setDeltaMovement(d4, d5, d6); +- this.level.getProfiler().push("ai"); ++ //this.level.getProfiler().push("ai"); // Purpur + if (this.isImmobile()) { + this.jumping = false; + this.xxa = 0.0F; + this.zza = 0.0F; + } else if (this.isEffectiveAi()) { +- this.level.getProfiler().push("newAi"); ++ //this.level.getProfiler().push("newAi"); // Purpur + this.serverAiStep(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("jump"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("jump"); // Purpur + if (this.jumping && this.isAffectedByFluids()) { + double d7; + +@@ -3371,8 +3436,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.noJumpDelay = 0; + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("travel"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("travel"); // Purpur + this.xxa *= 0.98F; + this.zza *= 0.98F; + this.updateFallFlying(); +@@ -3388,8 +3453,8 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + //SpigotTimings.timerEntityAIMove.stopTiming(); // Spigot // Paper + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("freezing"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("freezing"); // Purpur + if (!this.level.isClientSide && !this.isDeadOrDying() && !freezeLocked) { // Paper - Freeze Tick Lock API + int i = this.getTicksFrozen(); + +@@ -3406,18 +3471,20 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.hurt(this.damageSources().freeze(), 1.0F); + } + +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("push"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("push"); // Purpur + if (this.autoSpinAttackTicks > 0) { + --this.autoSpinAttackTicks; + this.checkAutoSpinAttack(axisalignedbb, this.getBoundingBox()); + } + + this.pushEntities(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + // Paper start +- if (((ServerLevel) this.level).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { +- if (this.xo != getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { ++ // Purpur start ++ if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { ++ if (((ServerLevel) this.level).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { ++ // Purpur end + Location from = new Location(this.level.getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); + Location to = new Location (this.level.getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); + io.papermc.paper.event.entity.EntityMoveEvent event = new io.papermc.paper.event.entity.EntityMoveEvent(this.getBukkitLivingEntity(), from, to.clone()); +@@ -3427,12 +3494,48 @@ public abstract class LivingEntity extends Entity implements Attackable { + absMoveTo(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(), event.getTo().getYaw(), event.getTo().getPitch()); + } + } ++ // Purpur start ++ if (getRider() != null) { ++ getRider().resetLastActionTime(); ++ if (((ServerLevel) level).hasRidableMoveEvent && this instanceof Mob) { ++ Location from = new Location(level.getWorld(), xo, yo, zo, this.yRotO, this.xRotO); ++ Location to = new Location(level.getWorld(), getX(), getY(), getZ(), this.getYRot(), this.getXRot()); ++ org.purpurmc.purpur.event.entity.RidableMoveEvent event = new org.purpurmc.purpur.event.entity.RidableMoveEvent((org.bukkit.entity.Mob) getBukkitLivingEntity(), (Player) getRider().getBukkitEntity(), from, to.clone()); ++ if (!event.callEvent()) { ++ absMoveTo(from.getX(), from.getY(), from.getZ(), from.getYaw(), from.getPitch()); ++ } else if (!to.equals(event.getTo())) { ++ absMoveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); ++ } ++ } ++ } ++ // Purpur end + } + // Paper end + if (!this.level.isClientSide && this.isSensitiveToWater() && this.isInWaterRainOrBubble()) { + this.hurt(this.damageSources().drown(), 1.0F); + } + ++ // Purpur start - copied from Zombie ++ if (this.isAlive()) { ++ boolean flag = this.shouldBurnInDay() && this.isSunBurnTick(); ++ if (flag) { ++ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); ++ if (!itemstack.isEmpty()) { ++ if (itemstack.isDamageableItem()) { ++ itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); ++ if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { ++ this.broadcastBreakEvent(EquipmentSlot.HEAD); ++ this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); ++ } ++ } ++ flag = false; ++ } ++ if (flag) { ++ this.setSecondsOnFire(8); ++ } ++ } ++ } ++ // Purpur end + } + + public boolean isSensitiveToWater() { +@@ -3453,7 +3556,16 @@ public abstract class LivingEntity extends Entity implements Attackable { + int j = i / 10; + + if (j % 2 == 0) { +- itemstack.hurtAndBreak(1, this, (entityliving) -> { ++ // Purpur start ++ int damage = level.purpurConfig.elytraDamagePerSecond; ++ if (level.purpurConfig.elytraDamageMultiplyBySpeed > 0) { ++ double speed = getDeltaMovement().lengthSqr(); ++ if (speed > level.purpurConfig.elytraDamageMultiplyBySpeed) { ++ damage *= (int) speed; ++ } ++ } ++ itemstack.hurtAndBreak(damage, this, (entityliving) -> { ++ // Purpur end + entityliving.broadcastBreakEvent(EquipmentSlot.CHEST); + }); + } +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 636e601b004a412d02e5be86e97d489b52c28e1b..8e2274f7dce34e0997356205cf96e46f8d41cca1 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -65,6 +65,7 @@ import net.minecraft.world.item.ProjectileWeaponItem; + import net.minecraft.world.item.SpawnEggItem; + import net.minecraft.world.item.SwordItem; + import net.minecraft.world.item.enchantment.EnchantmentHelper; ++import net.minecraft.world.item.enchantment.Enchantments; + import net.minecraft.world.level.GameRules; + import net.minecraft.world.level.ItemLike; + import net.minecraft.world.level.Level; +@@ -133,6 +134,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + private BlockPos restrictCenter; + private float restrictRadius; + ++ public int ticksSinceLastInteraction; // Purpur + public boolean aware = true; // CraftBukkit + + protected Mob(EntityType type, Level world) { +@@ -146,8 +148,8 @@ public abstract class Mob extends LivingEntity implements Targeting { + this.restrictRadius = -1.0F; + this.goalSelector = new GoalSelector(world.getProfilerSupplier()); + this.targetSelector = new GoalSelector(world.getProfilerSupplier()); +- this.lookControl = new LookControl(this); +- this.moveControl = new MoveControl(this); ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this); // Purpur ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this); // Purpur + this.jumpControl = new JumpControl(this); + this.bodyRotationControl = this.createBodyControl(); + this.navigation = this.createNavigation(world); +@@ -319,6 +321,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + entityliving = null; + } + } ++ if (entityliving instanceof ServerPlayer) this.ticksSinceLastInteraction = 0; // Purpur + this.target = entityliving; + return true; + // CraftBukkit end +@@ -359,15 +362,35 @@ public abstract class Mob extends LivingEntity implements Targeting { + @Override + public void baseTick() { + super.baseTick(); +- this.level.getProfiler().push("mobBaseTick"); ++ //this.level.getProfiler().push("mobBaseTick"); // Purpur + if (this.isAlive() && this.random.nextInt(1000) < this.ambientSoundTime++) { + this.resetAmbientSoundTime(); + this.playAmbientSound(); + } + +- this.level.getProfiler().pop(); ++ incrementTicksSinceLastInteraction(); // Purpur ++ //this.level.getProfiler().pop(); // Purpur + } + ++ // Purpur start ++ private void incrementTicksSinceLastInteraction() { ++ ++this.ticksSinceLastInteraction; ++ if (getRider() != null) { ++ this.ticksSinceLastInteraction = 0; ++ return; ++ } ++ if (this.level.purpurConfig.entityLifeSpan <= 0) { ++ return; // feature disabled ++ } ++ if (!this.removeWhenFarAway(0) || isPersistenceRequired() || requiresCustomPersistence() || hasCustomName()) { ++ return; // mob persistent ++ } ++ if (this.ticksSinceLastInteraction > this.level.purpurConfig.entityLifeSpan) { ++ this.discard(); ++ } ++ } ++ // Purpur end ++ + @Override + protected void playHurtSound(DamageSource source) { + this.resetAmbientSoundTime(); +@@ -557,6 +580,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + } + + nbt.putBoolean("Bukkit.Aware", this.aware); // CraftBukkit ++ nbt.putInt("Purpur.ticksSinceLastInteraction", this.ticksSinceLastInteraction); // Purpur + } + + @Override +@@ -627,6 +651,11 @@ public abstract class Mob extends LivingEntity implements Targeting { + this.aware = nbt.getBoolean("Bukkit.Aware"); + } + // CraftBukkit end ++ // Purpur start ++ if (nbt.contains("Purpur.ticksSinceLastInteraction")) { ++ this.ticksSinceLastInteraction = nbt.getInt("Purpur.ticksSinceLastInteraction"); ++ } ++ // Purpur end + } + + @Override +@@ -670,8 +699,8 @@ public abstract class Mob extends LivingEntity implements Targeting { + @Override + public void aiStep() { + super.aiStep(); +- this.level.getProfiler().push("looting"); +- if (!this.level.isClientSide && this.canPickUpLoot() && this.isAlive() && !this.dead && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ //this.level.getProfiler().push("looting"); // Purpur ++ if (!this.level.isClientSide && this.canPickUpLoot() && this.isAlive() && !this.dead && (this.level.purpurConfig.entitiesPickUpLootBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { + Vec3i baseblockposition = this.getPickupReach(); + List list = this.level.getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate((double) baseblockposition.getX(), (double) baseblockposition.getY(), (double) baseblockposition.getZ())); + Iterator iterator = list.iterator(); +@@ -690,7 +719,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + } + } + +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + + protected Vec3i getPickupReach() { +@@ -902,46 +931,46 @@ public abstract class Mob extends LivingEntity implements Targeting { + return; + } + // Paper end +- this.level.getProfiler().push("sensing"); ++ //this.level.getProfiler().push("sensing"); // Purpur + this.sensing.tick(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + int i = this.level.getServer().getTickCount() + this.getId(); + + if (i % 2 != 0 && this.tickCount > 1) { +- this.level.getProfiler().push("targetSelector"); ++ //this.level.getProfiler().push("targetSelector"); // Purpur + if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.targetSelector.tickRunningGoals(false); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("goalSelector"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("goalSelector"); // Purpur + if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.goalSelector.tickRunningGoals(false); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } else { +- this.level.getProfiler().push("targetSelector"); ++ //this.level.getProfiler().push("targetSelector"); // Purpur + if (this.targetSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.targetSelector.tick(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("goalSelector"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("goalSelector"); // Purpur + if (this.goalSelector.inactiveTick(this.activatedPriority, false)) // Pufferfish - use this to alternate ticking + this.goalSelector.tick(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + +- this.level.getProfiler().push("navigation"); ++ //this.level.getProfiler().push("navigation"); // Purpur + this.navigation.tick(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("mob tick"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("mob tick"); // Purpur + this.customServerAiStep(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("controls"); +- this.level.getProfiler().push("move"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("controls"); // Purpur ++ //this.level.getProfiler().push("move"); // Purpur + this.moveControl.tick(); +- this.level.getProfiler().popPush("look"); ++ //this.level.getProfiler().popPush("look"); // Purpur + this.lookControl.tick(); +- this.level.getProfiler().popPush("jump"); ++ //this.level.getProfiler().popPush("jump"); // Purpur + this.jumpControl.tick(); +- this.level.getProfiler().pop(); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().pop(); // Purpur + this.sendDebugPackets(); + } + +@@ -1163,6 +1192,12 @@ public abstract class Mob extends LivingEntity implements Targeting { + + } + ++ // Purpur start ++ public static @Nullable EquipmentSlot getSlotForDispenser(ItemStack itemstack) { ++ return EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BINDING_CURSE, itemstack) > 0 ? null : getEquipmentSlotForItem(itemstack); ++ } ++ // Purpur end ++ + @Nullable + public static Item getEquipmentForSlot(EquipmentSlot equipmentSlot, int equipmentLevel) { + switch (equipmentSlot) { +@@ -1257,7 +1292,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + RandomSource randomsource = world.getRandom(); + + this.getAttribute(Attributes.FOLLOW_RANGE).addPermanentModifier(new AttributeModifier("Random spawn bonus", randomsource.triangle(0.0D, 0.11485000000000001D), AttributeModifier.Operation.MULTIPLY_BASE)); +- if (randomsource.nextFloat() < 0.05F) { ++ if (randomsource.nextFloat() < world.getLevel().purpurConfig.entityLeftHandedChance) { // Purpur + this.setLeftHanded(true); + } else { + this.setLeftHanded(false); +@@ -1305,6 +1340,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + if (!this.isAlive()) { + return InteractionResult.PASS; + } else if (this.getLeashHolder() == player) { ++ if (hand == InteractionHand.OFF_HAND && (level.purpurConfig.villagerCanBeLeashed || level.purpurConfig.wanderingTraderCanBeLeashed) && this instanceof net.minecraft.world.entity.npc.AbstractVillager) return InteractionResult.CONSUME; // Purpur + // CraftBukkit start - fire PlayerUnleashEntityEvent + // Paper start - drop leash variable + org.bukkit.event.player.PlayerUnleashEntityEvent event = CraftEventFactory.callPlayerUnleashEntityEvent(this, player, hand, !player.getAbilities().instabuild); +@@ -1378,7 +1414,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + protected void onOffspringSpawnedFromEgg(Player player, Mob child) {} + + protected InteractionResult mobInteract(Player player, InteractionHand hand) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + public boolean isWithinRestriction() { +@@ -1684,6 +1720,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + this.setLastHurtMob(target); + } + ++ if (target instanceof ServerPlayer) this.ticksSinceLastInteraction = 0; // Purpur + return flag; + } + +@@ -1700,17 +1737,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + } + + public boolean isSunBurnTick() { +- if (this.level.isDay() && !this.level.isClientSide) { +- float f = this.getLightLevelDependentMagicValue(); +- BlockPos blockposition = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); +- boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; +- +- if (f > 0.5F && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && !flag && this.level.canSeeSky(blockposition)) { +- return true; +- } +- } +- +- return false; ++ return super.isSunBurnTick(); // Purpur - moved contents to Entity + } + + @Override +@@ -1754,4 +1781,56 @@ public abstract class Mob extends LivingEntity implements Targeting { + + return itemmonsteregg == null ? null : new ItemStack(itemmonsteregg); + } ++ ++ // Purpur start ++ public double getMaxY() { ++ return level.getHeight(); ++ } ++ ++ public InteractionResult tryRide(Player player, InteractionHand hand) { ++ return tryRide(player, hand, InteractionResult.PASS); ++ } ++ ++ public InteractionResult tryRide(Player player, InteractionHand hand, InteractionResult result) { ++ if (!isRidable()) { ++ return result; ++ } ++ if (hand != InteractionHand.MAIN_HAND) { ++ return InteractionResult.PASS; ++ } ++ if (player.isShiftKeyDown()) { ++ return InteractionResult.PASS; ++ } ++ if (!player.getItemInHand(hand).isEmpty()) { ++ return InteractionResult.PASS; ++ } ++ if (!passengers.isEmpty() || player.isPassenger()) { ++ return InteractionResult.PASS; ++ } ++ if (this instanceof TamableAnimal tamable) { ++ if (tamable.isTame() && !tamable.isOwnedBy(player)) { ++ return InteractionResult.PASS; ++ } ++ if (!tamable.isTame() && !level.purpurConfig.untamedTamablesAreRidable) { ++ return InteractionResult.PASS; ++ } ++ } ++ if (this instanceof AgeableMob ageable) { ++ if (ageable.isBaby() && !level.purpurConfig.babiesAreRidable) { ++ return InteractionResult.PASS; ++ } ++ } ++ if (!player.getBukkitEntity().hasPermission("allow.ride." + net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(getType()).getPath())) { ++ player.sendMiniMessage(org.purpurmc.purpur.PurpurConfig.cannotRideMob); ++ return InteractionResult.PASS; ++ } ++ player.setYRot(this.getYRot()); ++ player.setXRot(this.getXRot()); ++ if (player.startRiding(this)) { ++ return InteractionResult.SUCCESS; ++ } else { ++ return InteractionResult.PASS; ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/Shearable.java b/src/main/java/net/minecraft/world/entity/Shearable.java +index 5e8cc5cfac8888628c6d513148f41be09ca65a2c..a089fc61ec09be6b7490375489178dc6ba5a644b 100644 +--- a/src/main/java/net/minecraft/world/entity/Shearable.java ++++ b/src/main/java/net/minecraft/world/entity/Shearable.java +@@ -3,7 +3,13 @@ package net.minecraft.world.entity; + import net.minecraft.sounds.SoundSource; + + public interface Shearable { +- void shear(SoundSource shearedSoundCategory); ++ // Purpur start ++ default void shear(SoundSource shearedSoundCategory) { ++ shear(shearedSoundCategory, 0); ++ } ++ ++ void shear(SoundSource shearedSoundCategory, int looting); ++ // Purpur end + + boolean readyForShearing(); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +index e283eb57c25f7de222f9d09dca851169f5f6e488..210a0bee1227e4671909dd553ab22027cfc868fb 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/AttributeMap.java +@@ -24,14 +24,21 @@ public class AttributeMap { + private final Set dirtyAttributes = Sets.newHashSet(); + private final AttributeSupplier supplier; + private final java.util.function.Function createInstance; // Pufferfish ++ private final net.minecraft.world.entity.LivingEntity entity; // Purpur + + public AttributeMap(AttributeSupplier defaultAttributes) { ++ // Purpur start ++ this(defaultAttributes, null); ++ } ++ public AttributeMap(AttributeSupplier defaultAttributes, net.minecraft.world.entity.LivingEntity entity) { ++ this.entity = entity; ++ // Purpur end + this.supplier = defaultAttributes; + this.createInstance = attribute -> this.supplier.createInstance(this::onAttributeModified, attribute); // Pufferfish + } + + private void onAttributeModified(AttributeInstance instance) { +- if (instance.getAttribute().isClientSyncable()) { ++ if (instance.getAttribute().isClientSyncable() && (entity == null || entity.shouldSendAttribute(instance.getAttribute()))) { // Purpur + this.dirtyAttributes.add(instance); + } + +@@ -43,7 +50,7 @@ public class AttributeMap { + + public Collection getSyncableAttributes() { + return this.attributes.values().stream().filter((attribute) -> { +- return attribute.getAttribute().isClientSyncable(); ++ return attribute.getAttribute().isClientSyncable() && (entity == null || entity.shouldSendAttribute(attribute.getAttribute())); // Purpur + }).collect(Collectors.toList()); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java +index 8a720f9ae81d7ea856e28cb27a66adcf04bcb0eb..e0b70d9732a2b7d96999b7e4a497ffa1d8cf86a7 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java +@@ -80,7 +80,88 @@ import org.slf4j.Logger; + + public class DefaultAttributes { + private static final Logger LOGGER = LogUtils.getLogger(); +- private static final Map, AttributeSupplier> SUPPLIERS = ImmutableMap., AttributeSupplier>builder().put(EntityType.ALLAY, Allay.createAttributes().build()).put(EntityType.ARMOR_STAND, LivingEntity.createLivingAttributes().build()).put(EntityType.AXOLOTL, Axolotl.createAttributes().build()).put(EntityType.BAT, Bat.createAttributes().build()).put(EntityType.BEE, Bee.createAttributes().build()).put(EntityType.BLAZE, Blaze.createAttributes().build()).put(EntityType.CAT, Cat.createAttributes().build()).put(EntityType.CAMEL, Camel.createAttributes().build()).put(EntityType.CAVE_SPIDER, CaveSpider.createCaveSpider().build()).put(EntityType.CHICKEN, Chicken.createAttributes().build()).put(EntityType.COD, AbstractFish.createAttributes().build()).put(EntityType.COW, Cow.createAttributes().build()).put(EntityType.CREEPER, Creeper.createAttributes().build()).put(EntityType.DOLPHIN, Dolphin.createAttributes().build()).put(EntityType.DONKEY, AbstractChestedHorse.createBaseChestedHorseAttributes().build()).put(EntityType.DROWNED, Zombie.createAttributes().build()).put(EntityType.ELDER_GUARDIAN, ElderGuardian.createAttributes().build()).put(EntityType.ENDERMAN, EnderMan.createAttributes().build()).put(EntityType.ENDERMITE, Endermite.createAttributes().build()).put(EntityType.ENDER_DRAGON, EnderDragon.createAttributes().build()).put(EntityType.EVOKER, Evoker.createAttributes().build()).put(EntityType.FOX, Fox.createAttributes().build()).put(EntityType.FROG, Frog.createAttributes().build()).put(EntityType.GHAST, Ghast.createAttributes().build()).put(EntityType.GIANT, Giant.createAttributes().build()).put(EntityType.GLOW_SQUID, GlowSquid.createAttributes().build()).put(EntityType.GOAT, Goat.createAttributes().build()).put(EntityType.GUARDIAN, Guardian.createAttributes().build()).put(EntityType.HOGLIN, Hoglin.createAttributes().build()).put(EntityType.HORSE, AbstractHorse.createBaseHorseAttributes().build()).put(EntityType.HUSK, Zombie.createAttributes().build()).put(EntityType.ILLUSIONER, Illusioner.createAttributes().build()).put(EntityType.IRON_GOLEM, IronGolem.createAttributes().build()).put(EntityType.LLAMA, Llama.createAttributes().build()).put(EntityType.MAGMA_CUBE, MagmaCube.createAttributes().build()).put(EntityType.MOOSHROOM, Cow.createAttributes().build()).put(EntityType.MULE, AbstractChestedHorse.createBaseChestedHorseAttributes().build()).put(EntityType.OCELOT, Ocelot.createAttributes().build()).put(EntityType.PANDA, Panda.createAttributes().build()).put(EntityType.PARROT, Parrot.createAttributes().build()).put(EntityType.PHANTOM, Monster.createMonsterAttributes().build()).put(EntityType.PIG, Pig.createAttributes().build()).put(EntityType.PIGLIN, Piglin.createAttributes().build()).put(EntityType.PIGLIN_BRUTE, PiglinBrute.createAttributes().build()).put(EntityType.PILLAGER, Pillager.createAttributes().build()).put(EntityType.PLAYER, Player.createAttributes().build()).put(EntityType.POLAR_BEAR, PolarBear.createAttributes().build()).put(EntityType.PUFFERFISH, AbstractFish.createAttributes().build()).put(EntityType.RABBIT, Rabbit.createAttributes().build()).put(EntityType.RAVAGER, Ravager.createAttributes().build()).put(EntityType.SALMON, AbstractFish.createAttributes().build()).put(EntityType.SHEEP, Sheep.createAttributes().build()).put(EntityType.SHULKER, Shulker.createAttributes().build()).put(EntityType.SILVERFISH, Silverfish.createAttributes().build()).put(EntityType.SKELETON, AbstractSkeleton.createAttributes().build()).put(EntityType.SKELETON_HORSE, SkeletonHorse.createAttributes().build()).put(EntityType.SLIME, Monster.createMonsterAttributes().build()).put(EntityType.SNIFFER, Sniffer.createAttributes().build()).put(EntityType.SNOW_GOLEM, SnowGolem.createAttributes().build()).put(EntityType.SPIDER, Spider.createAttributes().build()).put(EntityType.SQUID, Squid.createAttributes().build()).put(EntityType.STRAY, AbstractSkeleton.createAttributes().build()).put(EntityType.STRIDER, Strider.createAttributes().build()).put(EntityType.TADPOLE, Tadpole.createAttributes().build()).put(EntityType.TRADER_LLAMA, Llama.createAttributes().build()).put(EntityType.TROPICAL_FISH, AbstractFish.createAttributes().build()).put(EntityType.TURTLE, Turtle.createAttributes().build()).put(EntityType.VEX, Vex.createAttributes().build()).put(EntityType.VILLAGER, Villager.createAttributes().build()).put(EntityType.VINDICATOR, Vindicator.createAttributes().build()).put(EntityType.WARDEN, Warden.createAttributes().build()).put(EntityType.WANDERING_TRADER, Mob.createMobAttributes().build()).put(EntityType.WITCH, Witch.createAttributes().build()).put(EntityType.WITHER, WitherBoss.createAttributes().build()).put(EntityType.WITHER_SKELETON, AbstractSkeleton.createAttributes().build()).put(EntityType.WOLF, Wolf.createAttributes().build()).put(EntityType.ZOGLIN, Zoglin.createAttributes().build()).put(EntityType.ZOMBIE, Zombie.createAttributes().build()).put(EntityType.ZOMBIE_HORSE, ZombieHorse.createAttributes().build()).put(EntityType.ZOMBIE_VILLAGER, Zombie.createAttributes().build()).put(EntityType.ZOMBIFIED_PIGLIN, ZombifiedPiglin.createAttributes().build()).build(); ++ private static final Map, AttributeSupplier> SUPPLIERS = ImmutableMap., AttributeSupplier>builder() ++ .put(EntityType.ALLAY, Allay.createAttributes().build()) ++ .put(EntityType.ARMOR_STAND, LivingEntity.createLivingAttributes().build()) ++ .put(EntityType.AXOLOTL, Axolotl.createAttributes().build()) ++ .put(EntityType.BAT, Bat.createAttributes().build()) ++ .put(EntityType.BEE, Bee.createAttributes().build()) ++ .put(EntityType.BLAZE, Blaze.createAttributes().build()) ++ .put(EntityType.CAT, Cat.createAttributes().build()) ++ .put(EntityType.CAMEL, Camel.createAttributes().build()) ++ .put(EntityType.CAVE_SPIDER, CaveSpider.createCaveSpider().build()) ++ .put(EntityType.CHICKEN, Chicken.createAttributes().build()) ++ .put(EntityType.COD, AbstractFish.createAttributes().build()) ++ .put(EntityType.COW, Cow.createAttributes().build()) ++ .put(EntityType.CREEPER, Creeper.createAttributes().build()) ++ .put(EntityType.DOLPHIN, Dolphin.createAttributes().build()) ++ .put(EntityType.DONKEY, AbstractChestedHorse.createBaseChestedHorseAttributes().build()) ++ .put(EntityType.DROWNED, Zombie.createAttributes().build()) ++ .put(EntityType.ELDER_GUARDIAN, ElderGuardian.createAttributes().build()) ++ .put(EntityType.ENDERMAN, EnderMan.createAttributes().build()) ++ .put(EntityType.ENDERMITE, Endermite.createAttributes().build()) ++ .put(EntityType.ENDER_DRAGON, EnderDragon.createAttributes().build()) ++ .put(EntityType.EVOKER, Evoker.createAttributes().build()) ++ .put(EntityType.FOX, Fox.createAttributes().build()) ++ .put(EntityType.FROG, Frog.createAttributes().build()) ++ .put(EntityType.GHAST, Ghast.createAttributes().build()) ++ .put(EntityType.GIANT, Giant.createAttributes().build()) ++ .put(EntityType.GLOW_SQUID, GlowSquid.createAttributes().build()) ++ .put(EntityType.GOAT, Goat.createAttributes().build()) ++ .put(EntityType.GUARDIAN, Guardian.createAttributes().build()) ++ .put(EntityType.HOGLIN, Hoglin.createAttributes().build()) ++ .put(EntityType.HORSE, AbstractHorse.createBaseHorseAttributes().build()) ++ .put(EntityType.HUSK, Zombie.createAttributes().build()) ++ .put(EntityType.ILLUSIONER, Illusioner.createAttributes().build()) ++ .put(EntityType.IRON_GOLEM, IronGolem.createAttributes().build()) ++ .put(EntityType.LLAMA, Llama.createAttributes().build()) ++ .put(EntityType.MAGMA_CUBE, MagmaCube.createAttributes().build()) ++ .put(EntityType.MOOSHROOM, Cow.createAttributes().build()) ++ .put(EntityType.MULE, AbstractChestedHorse.createBaseChestedHorseAttributes().build()) ++ .put(EntityType.OCELOT, Ocelot.createAttributes().build()) ++ .put(EntityType.PANDA, Panda.createAttributes().build()) ++ .put(EntityType.PARROT, Parrot.createAttributes().build()) ++ .put(EntityType.PHANTOM, net.minecraft.world.entity.monster.Phantom.createAttributes().build()) // Purpur ++ .put(EntityType.PIG, Pig.createAttributes().build()) ++ .put(EntityType.PIGLIN, Piglin.createAttributes().build()) ++ .put(EntityType.PIGLIN_BRUTE, PiglinBrute.createAttributes().build()) ++ .put(EntityType.PILLAGER, Pillager.createAttributes().build()) ++ .put(EntityType.PLAYER, Player.createAttributes().build()) ++ .put(EntityType.POLAR_BEAR, PolarBear.createAttributes().build()) ++ .put(EntityType.PUFFERFISH, AbstractFish.createAttributes().build()) ++ .put(EntityType.RABBIT, Rabbit.createAttributes().build()) ++ .put(EntityType.RAVAGER, Ravager.createAttributes().build()) ++ .put(EntityType.SALMON, AbstractFish.createAttributes().build()) ++ .put(EntityType.SHEEP, Sheep.createAttributes().build()) ++ .put(EntityType.SHULKER, Shulker.createAttributes().build()) ++ .put(EntityType.SILVERFISH, Silverfish.createAttributes().build()) ++ .put(EntityType.SKELETON, AbstractSkeleton.createAttributes().build()) ++ .put(EntityType.SKELETON_HORSE, SkeletonHorse.createAttributes().build()) ++ .put(EntityType.SLIME, Monster.createMonsterAttributes().build()) ++ .put(EntityType.SNIFFER, Sniffer.createAttributes().build()) ++ .put(EntityType.SNOW_GOLEM, SnowGolem.createAttributes().build()) ++ .put(EntityType.SPIDER, Spider.createAttributes().build()) ++ .put(EntityType.SQUID, Squid.createAttributes().build()) ++ .put(EntityType.STRAY, AbstractSkeleton.createAttributes().build()) ++ .put(EntityType.STRIDER, Strider.createAttributes().build()) ++ .put(EntityType.TADPOLE, Tadpole.createAttributes().build()) ++ .put(EntityType.TRADER_LLAMA, Llama.createAttributes().build()) ++ .put(EntityType.TROPICAL_FISH, AbstractFish.createAttributes().build()) ++ .put(EntityType.TURTLE, Turtle.createAttributes().build()) ++ .put(EntityType.VEX, Vex.createAttributes().build()) ++ .put(EntityType.VILLAGER, Villager.createAttributes().build()) ++ .put(EntityType.VINDICATOR, Vindicator.createAttributes().build()) ++ .put(EntityType.WARDEN, Warden.createAttributes().build()) ++ .put(EntityType.WANDERING_TRADER, Mob.createMobAttributes().build()) ++ .put(EntityType.WITCH, Witch.createAttributes().build()) ++ .put(EntityType.WITHER, WitherBoss.createAttributes().build()) ++ .put(EntityType.WITHER_SKELETON, AbstractSkeleton.createAttributes().build()) ++ .put(EntityType.WOLF, Wolf.createAttributes().build()) ++ .put(EntityType.ZOGLIN, Zoglin.createAttributes().build()) ++ .put(EntityType.ZOMBIE, Zombie.createAttributes().build()) ++ .put(EntityType.ZOMBIE_HORSE, ZombieHorse.createAttributes().build()) ++ .put(EntityType.ZOMBIE_VILLAGER, Zombie.createAttributes().build()) ++ .put(EntityType.ZOMBIFIED_PIGLIN, ZombifiedPiglin.createAttributes().build()).build(); + + public static AttributeSupplier getSupplier(EntityType type) { + return SUPPLIERS.get(type); +diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java b/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java +index f0703302e7dbbda88de8c648d20d87c55ed9b1e0..a913ebabaa5f443afa987b972355a8f8d1723c78 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java ++++ b/src/main/java/net/minecraft/world/entity/ai/attributes/RangedAttribute.java +@@ -29,6 +29,7 @@ public class RangedAttribute extends Attribute { + + @Override + public double sanitizeValue(double value) { ++ if (!org.purpurmc.purpur.PurpurConfig.clampAttributes) return Double.isNaN(value) ? this.minValue : value; // Purpur + return Double.isNaN(value) ? this.minValue : Mth.clamp(value, this.minValue, this.maxValue); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java +index 57ef7fbba3028c28231abf7b7ae78aa019323536..651c156dc8a5aad04d461add02e22147af657d07 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java +@@ -58,9 +58,9 @@ public abstract class Behavior implements BehaviorContro + this.status = Behavior.Status.RUNNING; + int i = this.minDuration + world.getRandom().nextInt(this.maxDuration + 1 - this.minDuration); + this.endTimestamp = time + (long)i; +- this.timing.startTiming(); // Paper - behavior timings ++ //this.timing.startTiming(); // Paper - behavior timings // Purpur + this.start(world, entity, time); +- this.timing.stopTiming(); // Paper - behavior timings ++ //this.timing.stopTiming(); // Paper - behavior timings // Purpur + return true; + } else { + return false; +@@ -72,13 +72,13 @@ public abstract class Behavior implements BehaviorContro + + @Override + public final void tickOrStop(ServerLevel world, E entity, long time) { +- this.timing.startTiming(); // Paper - behavior timings ++ //this.timing.startTiming(); // Paper - behavior timings // Purpur + if (!this.timedOut(time) && this.canStillUse(world, entity, time)) { + this.tick(world, entity, time); + } else { + this.doStop(world, entity, time); + } +- this.timing.stopTiming(); // Paper - behavior timings ++ //this.timing.stopTiming(); // Paper - behavior timings // Purpur + + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java b/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java +index 7ad71f2c139c2288b49d6b0fde3f8b8013f5e095..2dca8e45b9b1f5451db2734cba4c2b03c9dd303b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java +@@ -34,17 +34,19 @@ public class HarvestFarmland extends Behavior { + private long nextOkStartTime; + private int timeWorkedSoFar; + private final List validFarmlandAroundVillager = Lists.newArrayList(); ++ private boolean clericWartFarmer = false; // Purpur + + public HarvestFarmland() { + super(ImmutableMap.of(MemoryModuleType.LOOK_TARGET, MemoryStatus.VALUE_ABSENT, MemoryModuleType.WALK_TARGET, MemoryStatus.VALUE_ABSENT, MemoryModuleType.SECONDARY_JOB_SITE, MemoryStatus.VALUE_PRESENT)); + } + + protected boolean checkExtraStartConditions(ServerLevel world, Villager entity) { +- if (!world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!world.purpurConfig.villagerBypassMobGriefing && !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; +- } else if (entity.getVillagerData().getProfession() != VillagerProfession.FARMER) { ++ } else if (entity.getVillagerData().getProfession() != VillagerProfession.FARMER && !(world.purpurConfig.villagerClericsFarmWarts && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC)) { // Purpur + return false; + } else { ++ if (!this.clericWartFarmer && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC) this.clericWartFarmer = true; // Purpur + BlockPos.MutableBlockPos blockposition_mutableblockposition = entity.blockPosition().mutable(); + + this.validFarmlandAroundVillager.clear(); +@@ -75,6 +77,7 @@ public class HarvestFarmland extends Behavior { + Block block = iblockdata.getBlock(); + Block block1 = world.getBlockState(pos.below()).getBlock(); + ++ if (this.clericWartFarmer) return block == Blocks.NETHER_WART && iblockdata.getValue(net.minecraft.world.level.block.NetherWartBlock.AGE) == 3 || iblockdata.isAir() && block1 == Blocks.SOUL_SAND; // Purpur + return block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata) || iblockdata.isAir() && block1 instanceof FarmBlock; + } + +@@ -100,7 +103,7 @@ public class HarvestFarmland extends Behavior { + Block block = iblockdata.getBlock(); + Block block1 = world.getBlockState(this.aboveFarmlandPos.below()).getBlock(); + +- if (block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata)) { ++ if (block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata) && !this.clericWartFarmer || this.clericWartFarmer && block == Blocks.NETHER_WART && iblockdata.getValue(net.minecraft.world.level.block.NetherWartBlock.AGE) == 3) { // Purpur + // CraftBukkit start + if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, this.aboveFarmlandPos, Blocks.AIR.defaultBlockState()).isCancelled()) { + world.destroyBlock(this.aboveFarmlandPos, true, entity); +@@ -108,7 +111,7 @@ public class HarvestFarmland extends Behavior { + // CraftBukkit end + } + +- if (iblockdata.isAir() && block1 instanceof FarmBlock && entity.hasFarmSeeds()) { ++ if (iblockdata.isAir() && (block1 instanceof FarmBlock && !this.clericWartFarmer || this.clericWartFarmer && block1 == Blocks.SOUL_SAND) && entity.hasFarmSeeds()) { // Purpur + SimpleContainer inventorysubcontainer = entity.getInventory(); + + for (int j = 0; j < inventorysubcontainer.getContainerSize(); ++j) { +@@ -119,6 +122,12 @@ public class HarvestFarmland extends Behavior { + BlockState iblockdata1; + + // CraftBukkit start ++ // Purpur start ++ if (this.clericWartFarmer && itemstack.getItem() == Items.NETHER_WART) { ++ iblockdata1 = Blocks.NETHER_WART.defaultBlockState(); ++ flag = true; ++ } else ++ // Purpur end + if (itemstack.is(Items.WHEAT_SEEDS)) { + iblockdata1 = Blocks.WHEAT.defaultBlockState(); + flag = true; +@@ -145,7 +154,7 @@ public class HarvestFarmland extends Behavior { + } + + if (flag) { +- world.playSound((Player) null, (double) this.aboveFarmlandPos.getX(), (double) this.aboveFarmlandPos.getY(), (double) this.aboveFarmlandPos.getZ(), SoundEvents.CROP_PLANTED, SoundSource.BLOCKS, 1.0F, 1.0F); ++ world.playSound((Player) null, (double) this.aboveFarmlandPos.getX(), (double) this.aboveFarmlandPos.getY(), (double) this.aboveFarmlandPos.getZ(), this.clericWartFarmer ? SoundEvents.NETHER_WART_PLANTED : SoundEvents.CROP_PLANTED, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur + itemstack.shrink(1); + if (itemstack.isEmpty()) { + inventorysubcontainer.setItem(j, ItemStack.EMPTY); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java b/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java +index c3fb86dc3d94d3a0d2464f2dbb83cda2fb9f7bbe..fd77dd0c0bfaba57e5bdfd13f7a90241ecdf813a 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java +@@ -57,7 +57,7 @@ public class InteractWithDoor { + + if (iblockdata.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) && !DoorBlock.requiresRedstone(entityliving.level, iblockdata, blockposition)) { // Purpur + DoorBlock blockdoor = (DoorBlock) iblockdata.getBlock(); + + if (!blockdoor.isOpen(iblockdata)) { +@@ -79,7 +79,7 @@ public class InteractWithDoor { + + if (iblockdata1.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) && !DoorBlock.requiresRedstone(entityliving.level, iblockdata, blockposition1)) { // Purpur + DoorBlock blockdoor1 = (DoorBlock) iblockdata1.getBlock(); + + if (!blockdoor1.isOpen(iblockdata1)) { +@@ -122,7 +122,7 @@ public class InteractWithDoor { + + if (!iblockdata.is(BlockTags.WOODEN_DOORS, (blockbase_blockdata) -> { + return blockbase_blockdata.getBlock() instanceof DoorBlock; +- })) { ++ }) || DoorBlock.requiresRedstone(entity.level, iblockdata, blockposition)) { // Purpur + iterator.remove(); + } else { + DoorBlock blockdoor = (DoorBlock) iblockdata.getBlock(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java b/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java +index 98373e013748817209b811d4adbb40a8787242a6..567b501f4de7556e55e2418d2f5700b4e4265235 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/ShowTradesToPlayer.java +@@ -42,6 +42,7 @@ public class ShowTradesToPlayer extends Behavior { + + @Override + public boolean canStillUse(ServerLevel world, Villager entity, long time) { ++ if (!entity.level.purpurConfig.villagerDisplayTradeItem) return false; // Purpur + return this.checkExtraStartConditions(world, entity) && this.lookTime > 0 && entity.getBrain().getMemory(MemoryModuleType.INTERACTION_TARGET).isPresent(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java b/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java +index 3af715e2f3f3949af614a8fcebbc4a835d48ca49..ade1e411ea1f3b4c9a417265e77b0d6861b222f9 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/TradeWithVillager.java +@@ -56,6 +56,12 @@ public class TradeWithVillager extends Behavior { + throwHalfStack(entity, ImmutableSet.of(Items.WHEAT), villager); + } + ++ // Purpur start ++ if (world.purpurConfig.villagerClericsFarmWarts && world.purpurConfig.villagerClericFarmersThrowWarts && entity.getVillagerData().getProfession() == VillagerProfession.CLERIC && entity.getInventory().countItem(Items.NETHER_WART) > Items.NETHER_WART.getMaxStackSize() / 2) { ++ throwHalfStack(entity, ImmutableSet.of(Items.NETHER_WART), villager); ++ } ++ // Purpur end ++ + if (!this.trades.isEmpty() && entity.getInventory().hasAnyOf(this.trades)) { + throwHalfStack(entity, this.trades, villager); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java +index cd7a90ec1073b2b452ca70decefe6a594445003b..47672e48c1cae73cffe532d622b296343fc12ef0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java +@@ -30,8 +30,13 @@ public class VillagerGoalPackages { + } + + public static ImmutableList>> getWorkPackage(VillagerProfession profession, float speed) { ++ // Purpur start ++ return getWorkPackage(profession, speed, false); ++ } ++ public static ImmutableList>> getWorkPackage(VillagerProfession profession, float speed, boolean clericsFarmWarts) { ++ // Purpur end + WorkAtPoi workAtPoi; +- if (profession == VillagerProfession.FARMER) { ++ if (profession == VillagerProfession.FARMER || (clericsFarmWarts && profession == VillagerProfession.CLERIC)) { // Purpur + workAtPoi = new WorkAtComposter(); + } else { + workAtPoi = new WorkAtPoi(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java +index 0951c04533e7c39b969d041271684355770b53c2..02d4ba2ccdce99ca97614baa7c8e49213126af96 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java +@@ -123,8 +123,10 @@ public class VillagerMakeLove extends Behavior { + return Optional.empty(); + } + // CraftBukkit end +- parent.setAge(6000); +- partner.setAge(6000); ++ // Purpur start ++ parent.setAge(world.purpurConfig.villagerBreedingTicks); ++ partner.setAge(world.purpurConfig.villagerBreedingTicks); ++ // Purpur end + world.addFreshEntityWithPassengers(entityvillager2, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason + world.broadcastEntityEvent(entityvillager2, (byte) 12); + return Optional.of(entityvillager2); +diff --git a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java +index 3d7586142aa5116964e4fffc6198f55fc6da324b..b4fb0af5bffffb9f0de3a2452c22b09fe92d7129 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java ++++ b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java +@@ -29,6 +29,20 @@ public class MoveControl implements Control { + this.mob = entity; + } + ++ // Purpur start ++ public void setSpeedModifier(double speed) { ++ this.speedModifier = speed; ++ } ++ ++ public void setForward(float forward) { ++ this.strafeForwards = forward; ++ } ++ ++ public void setStrafe(float strafe) { ++ this.strafeRight = strafe; ++ } ++ // Purpur end ++ + public boolean hasWanted() { + return this.operation == MoveControl.Operation.MOVE_TO; + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java +index 7df56705a4a0de2dc4ff7ab133fc26612c219162..60d21d6171b9af20a4c6fcc0d564a31aaa4ecdba 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java ++++ b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java +@@ -3,7 +3,7 @@ package net.minecraft.world.entity.ai.control; + import net.minecraft.util.Mth; + import net.minecraft.world.entity.Mob; + +-public class SmoothSwimmingLookControl extends LookControl { ++public class SmoothSwimmingLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + private final int maxYRotFromCenter; + private static final int HEAD_TILT_X = 10; + private static final int HEAD_TILT_Y = 20; +@@ -14,7 +14,7 @@ public class SmoothSwimmingLookControl extends LookControl { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.lookAtCooldown > 0) { + --this.lookAtCooldown; + this.getYRotD().ifPresent((yaw) -> { +@@ -32,9 +32,9 @@ public class SmoothSwimmingLookControl extends LookControl { + } + + float f = Mth.wrapDegrees(this.mob.yHeadRot - this.mob.yBodyRot); +- if (f < (float)(-this.maxYRotFromCenter)) { ++ if (f < (float) (-this.maxYRotFromCenter)) { + this.mob.yBodyRot -= 4.0F; +- } else if (f > (float)this.maxYRotFromCenter) { ++ } else if (f > (float) this.maxYRotFromCenter) { + this.mob.yBodyRot += 4.0F; + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java +index 529435cf648d61f80a37f041cee3c6fc0b74ceb6..6c7195c93b5968845da35450e80022c70839487d 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java +@@ -32,7 +32,7 @@ public class BreakDoorGoal extends DoorInteractGoal { + + @Override + public boolean canUse() { +- return !super.canUse() ? false : (!this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.isValidDifficulty(this.mob.level.getDifficulty()) && !this.isOpen()); ++ return !super.canUse() ? false : ((!this.mob.level.purpurConfig.zombieBypassMobGriefing && !this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) ? false : this.isValidDifficulty(this.mob.level.getDifficulty()) && !this.isOpen()); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java +index 80aa539f7c6a6ee44338de084cdcdf5fb4ef996a..c55118a4d2237a33039b63dc797ccdb86b63344f 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/EatBlockGoal.java +@@ -31,6 +31,12 @@ public class EatBlockGoal extends Goal { + + @Override + public boolean canUse() { ++ // Purpur start ++ net.minecraft.world.level.chunk.LevelChunk chunk = this.mob.level.getChunkIfLoaded(this.mob.blockPosition()); ++ if (chunk == null || chunk.playerChunk == null || !((net.minecraft.server.level.ServerLevel) this.mob.level).getChunkSource().chunkMap.anyPlayerCloseEnoughForSpawning(chunk.playerChunk, this.mob.chunkPosition(), false)) { ++ return false; ++ } ++ // Purpur end + if (this.mob.getRandom().nextInt(this.mob.isBaby() ? 50 : 1000) != 0) { + return false; + } else { +@@ -69,7 +75,7 @@ public class EatBlockGoal extends Goal { + + if (EatBlockGoal.IS_TALL_GRASS.test(this.level.getBlockState(blockposition))) { + // CraftBukkit +- if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, Blocks.AIR.defaultBlockState(), !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { ++ if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, Blocks.AIR.defaultBlockState(), !this.level.purpurConfig.sheepBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // Purpur + this.level.destroyBlock(blockposition, false); + } + +@@ -79,7 +85,7 @@ public class EatBlockGoal extends Goal { + + if (this.level.getBlockState(blockposition1).is(Blocks.GRASS_BLOCK)) { + // CraftBukkit +- if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition1, Blocks.DIRT.defaultBlockState(), !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // Paper - Fix wrong block state ++ if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition1, Blocks.DIRT.defaultBlockState(), !this.level.purpurConfig.sheepBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // Paper - Fix wrong block state // Purpur + this.level.levelEvent(2001, blockposition1, Block.getId(Blocks.GRASS_BLOCK.defaultBlockState())); + this.level.setBlock(blockposition1, Blocks.DIRT.defaultBlockState(), 2); + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +index 1635818fc4b1788c0d397085239df6dd75b210ab..02978315bc2b828cc603ce7478408f3f82c249c2 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +@@ -105,8 +105,8 @@ public class GoalSelector { + } + + public void tick() { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("goalCleanup"); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("goalCleanup"); // Purpur + + for(WrappedGoal wrappedGoal : this.availableGoals) { + if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { +@@ -123,8 +123,8 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); +- profilerFiller.push("goalUpdate"); ++ //profilerFiller.pop(); // Purpur ++ //profilerFiller.push("goalUpdate"); // Purpur + + for(WrappedGoal wrappedGoal2 : this.availableGoals) { + // Paper start +@@ -144,13 +144,13 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + this.tickRunningGoals(true); + } + + public void tickRunningGoals(boolean tickAll) { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("goalTick"); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("goalTick"); // Purpur + + for(WrappedGoal wrappedGoal : this.availableGoals) { + if (wrappedGoal.isRunning() && (tickAll || wrappedGoal.requiresUpdateEveryTick())) { +@@ -158,7 +158,7 @@ public class GoalSelector { + } + } + +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + } + + public Set getAvailableGoals() { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +index 721971f7618751a2e95f1c49fdc48a9c0c672cab..ad30f2d678cfc4b0d693e84e6e152c63b1b3cbb8 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +@@ -22,6 +22,7 @@ public class LlamaFollowCaravanGoal extends Goal { + + @Override + public boolean canUse() { ++ if (!this.llama.level.purpurConfig.llamaJoinCaravans || !this.llama.shouldJoinCaravan) return false; // Purpur + if (!this.llama.isLeashed() && !this.llama.inCaravan()) { + List list = this.llama.level.getEntities(this.llama, this.llama.getBoundingBox().inflate(9.0D, 4.0D, 9.0D), (entity) -> { + EntityType entityType = entity.getType(); +@@ -71,6 +72,7 @@ public class LlamaFollowCaravanGoal extends Goal { + + @Override + public boolean canContinueToUse() { ++ if (!this.llama.shouldJoinCaravan) return false; // Purpur + if (this.llama.inCaravan() && this.llama.getCaravanHead().isAlive() && this.firstIsLeashed(this.llama, 0)) { + double d = this.llama.distanceToSqr(this.llama.getCaravanHead()); + if (d > 676.0D) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java +index 6558b0d4bea99948fdc2b51751f3cfdc239d4b67..d85dabebbbbe213e791b8a3be3c6df05b959e40c 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java +@@ -111,9 +111,9 @@ public class RangedBowAttackGoal extends Go + + this.mob.getMoveControl().strafe(this.strafingBackwards ? -0.5F : 0.5F, this.strafingClockwise ? 0.5F : -0.5F); + this.mob.lookAt(livingEntity, 30.0F, 30.0F); +- } else { ++ } //else { // Purpur - fix MC-121706 + this.mob.getLookControl().setLookAt(livingEntity, 30.0F, 30.0F); +- } ++ //} // Purpur + + if (this.mob.isUsingItem()) { + if (!bl && this.seeTime < -60) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +index d3e91faee8805e88d850740fb5de9e5c8288c48b..fe526ebf395ff9813b94284fc3f0142323d6a303 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +@@ -40,7 +40,7 @@ public class RemoveBlockGoal extends MoveToBlockGoal { + + @Override + public boolean canUse() { +- if (!this.removerMob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.removerMob.level.purpurConfig.zombieBypassMobGriefing && !this.removerMob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } else if (this.nextStartTick > 0) { + --this.nextStartTick; +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java +index 5c64905e90ccca6e0b347241ddf9cc3f71058b8e..3bd7521b131b2b40f807bdc7ab95e64cf9bcdadc 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java +@@ -63,7 +63,7 @@ public class RunAroundLikeCrazyGoal extends Goal { + int j = this.horse.getMaxTemper(); + + // CraftBukkit - fire EntityTameEvent +- if (j > 0 && this.horse.getRandom().nextInt(j) < i && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this.horse, ((org.bukkit.craftbukkit.entity.CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled()) { ++ if ((this.horse.level.purpurConfig.alwaysTameInCreative && ((Player) entity).getAbilities().instabuild) || (j > 0 && this.horse.getRandom().nextInt(j) < i && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this.horse, ((org.bukkit.craftbukkit.entity.CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled())) { // Purpur + this.horse.tameWithName((Player) entity); + return; + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java +index e241ae250f4f04a17ef2c583d00b065a4ca56a4c..02b567e4e808e1a809d285ef39e1abc54e1e6ad2 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/SwellGoal.java +@@ -54,6 +54,14 @@ public class SwellGoal extends Goal { + this.creeper.setSwellDir(-1); + } else { + this.creeper.setSwellDir(1); ++ // Purpur start ++ if (this.creeper.getLevel().purpurConfig.creeperEncircleTarget) { ++ net.minecraft.world.phys.Vec3 relative = this.creeper.position().subtract(this.target.position()); ++ relative = relative.yRot((float) Math.PI / 3).normalize().multiply(2, 2, 2); ++ net.minecraft.world.phys.Vec3 destination = this.target.position().add(relative); ++ this.creeper.getNavigation().moveTo(destination.x, destination.y, destination.z, 1); ++ } ++ // Purpur end + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java +index 79bb13c5614bab1f0749c5f8f57f762c6216c564..2cbc9adc8e417def48be03d08174a5833068ec65 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/TemptGoal.java +@@ -62,7 +62,7 @@ public class TemptGoal extends Goal { + } + + private boolean shouldFollow(LivingEntity entity) { +- return this.items.test(entity.getMainHandItem()) || this.items.test(entity.getOffhandItem()); ++ return (this.items.test(entity.getMainHandItem()) || this.items.test(entity.getOffhandItem())) && (!(this.mob instanceof net.minecraft.world.entity.npc.Villager villager) || !villager.isSleeping()); // Purpur Fix #512 + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +index 7ffe5bef3778d5971ea4ceadf3102725fd0d08cd..6d127ed3da899851ca95b2be6792e2abca1aca12 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -172,12 +172,12 @@ public abstract class PathNavigation { + } + } + // Paper end +- this.level.getProfiler().push("pathfind"); ++ //this.level.getProfiler().push("pathfind"); // Purpur + BlockPos blockPos = useHeadPos ? this.mob.blockPosition().above() : this.mob.blockPosition(); + int i = (int)(followRange + (float)range); + PathNavigationRegion pathNavigationRegion = new PathNavigationRegion(this.level, blockPos.offset(-i, -i, -i), blockPos.offset(i, i, i)); + Path path = this.pathFinder.findPath(pathNavigationRegion, this.mob, positions, followRange, distance, this.maxVisitedNodesMultiplier); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + if (path != null && path.getTarget() != null) { + this.targetPos = path.getTarget(); + this.reachRange = distance; +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +index cb1d91f9fe98f21c2afbe3894dfd9bca3bdd3ba6..d2703432af207c74ea8d298a784329c3219d2f13 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +@@ -22,6 +22,13 @@ public class SecondaryPoiSensor extends Sensor { + + @Override + protected void doTick(ServerLevel world, Villager entity) { ++ // Purpur start - make sure clerics don't wander to soul sand when the option is off ++ Brain brain = entity.getBrain(); ++ if (!world.purpurConfig.villagerClericsFarmWarts && entity.getVillagerData().getProfession() == net.minecraft.world.entity.npc.VillagerProfession.CLERIC) { ++ brain.eraseMemory(MemoryModuleType.SECONDARY_JOB_SITE); ++ return; ++ } ++ // Purpur end + ResourceKey resourceKey = world.dimension(); + BlockPos blockPos = entity.blockPosition(); + List list = Lists.newArrayList(); +@@ -38,7 +45,7 @@ public class SecondaryPoiSensor extends Sensor { + } + } + +- Brain brain = entity.getBrain(); ++ //Brain brain = entity.getBrain(); // Purpur - moved up + if (!list.isEmpty()) { + brain.setMemory(MemoryModuleType.SECONDARY_JOB_SITE, list); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java +index 288c6627906d07c0d223eacd84ae4eb31a349998..9babe636176da3c40598eb5bdac0919a1704eaa0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensing.java +@@ -26,9 +26,9 @@ public class Sensing { + } else if (this.unseen.contains(i)) { + return false; + } else { +- this.mob.level.getProfiler().push("hasLineOfSight"); ++ //this.mob.level.getProfiler().push("hasLineOfSight"); // Purpur + boolean bl = this.mob.hasLineOfSight(entity); +- this.mob.level.getProfiler().pop(); ++ //this.mob.level.getProfiler().pop(); // Purpur + if (bl) { + this.seen.add(i); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java +index fcdb9bde8e1605e30dde3e580491522d4b62cdc0..7094701d213c73ba47ace806962244c10fdf4dda 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/Sensor.java +@@ -46,10 +46,10 @@ public abstract class Sensor { + if (--this.timeToTick <= 0L) { + // Paper start - configurable sensor tick rate and timings + this.timeToTick = java.util.Objects.requireNonNullElse(world.paperConfig().tickRates.sensor.get(entity.getType(), this.configKey), this.scanRate); +- this.timing.startTiming(); ++ //this.timing.startTiming(); // Purpur + // Paper end + this.doTick(world, entity); +- this.timing.stopTiming(); // Paper - sensor timings ++ //this.timing.stopTiming(); // Paper - sensor timings // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +index e752c83df50fb9b670ecea2abc95426c2a009b6f..baa4f9026d31de92210300ecb8ee8c1b6d575435 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java ++++ b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +@@ -64,6 +64,10 @@ public class TargetingConditions { + return false; + } else if (this.selector != null && !this.selector.test(targetEntity)) { + return false; ++ // Purpur start ++ } else if (!targetEntity.level.purpurConfig.idleTimeoutTargetPlayer && targetEntity instanceof net.minecraft.server.level.ServerPlayer player && player.isAfk()) { ++ return false; ++ // Purpur end + } else { + if (baseEntity == null) { + if (this.isCombat && (!targetEntity.canBeSeenAsEnemy() || targetEntity.level.getDifficulty() == Difficulty.PEACEFUL)) { +diff --git a/src/main/java/net/minecraft/world/entity/ambient/Bat.java b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +index 59338d739b6130f667d151bc27646c4a346886a2..bfa1b84cba0dface99fcd3cf573be49f6f1f7d55 100644 +--- a/src/main/java/net/minecraft/world/entity/ambient/Bat.java ++++ b/src/main/java/net/minecraft/world/entity/ambient/Bat.java +@@ -18,6 +18,7 @@ import net.minecraft.world.entity.EntityDimensions; + import net.minecraft.world.entity.EntityType; + import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.MobSpawnType; ++import net.minecraft.world.entity.MoverType; + import net.minecraft.world.entity.Pose; + import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + import net.minecraft.world.entity.ai.attributes.Attributes; +@@ -41,12 +42,81 @@ public class Bat extends AmbientCreature { + + public Bat(EntityType type, Level world) { + super(type, world); ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.075F); // Purpur + if (!world.isClientSide) { + this.setResting(true); + } + + } + ++ // Purpur start ++ @Override ++ public boolean shouldSendAttribute(net.minecraft.world.entity.ai.attributes.Attribute attribute) { return attribute != Attributes.FLYING_SPEED; } // Fixes log spam on clients ++ ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.batRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.batRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.batControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.batMaxY; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ if (isResting()) { ++ setResting(false); ++ level.levelEvent(null, 1025, new BlockPos(this).above(), 0); ++ } ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(MoverType.SELF, mot.multiply(speed, 0.25, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.batMaxHealth); ++ this.getAttribute(Attributes.FOLLOW_RANGE).setBaseValue(this.level.purpurConfig.batFollowRange); ++ this.getAttribute(Attributes.KNOCKBACK_RESISTANCE).setBaseValue(this.level.purpurConfig.batKnockbackResistance); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level.purpurConfig.batMovementSpeed); ++ this.getAttribute(Attributes.FLYING_SPEED).setBaseValue(this.level.purpurConfig.batFlyingSpeed); ++ this.getAttribute(Attributes.ARMOR).setBaseValue(this.level.purpurConfig.batArmor); ++ this.getAttribute(Attributes.ARMOR_TOUGHNESS).setBaseValue(this.level.purpurConfig.batArmorToughness); ++ this.getAttribute(Attributes.ATTACK_KNOCKBACK).setBaseValue(this.level.purpurConfig.batAttackKnockback); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.batTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.batAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean isFlapping() { + return !this.isResting() && this.tickCount % Bat.TICKS_PER_FLAP == 0; +@@ -96,7 +166,7 @@ public class Bat extends AmbientCreature { + protected void pushEntities() {} + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + public boolean isResting() { +@@ -128,6 +198,14 @@ public class Bat extends AmbientCreature { + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z()); ++ return; ++ } ++ // Purpur end ++ + super.customServerAiStep(); + BlockPos blockposition = this.blockPosition(); + BlockPos blockposition1 = blockposition.above(); +@@ -241,7 +319,7 @@ public class Bat extends AmbientCreature { + int i = world.getMaxLocalRawBrightness(pos); + byte b0 = 4; + +- if (Bat.isHalloween()) { ++ if (Bat.isHalloweenSeason(world.getMinecraftWorld())) { // Purpur + b0 = 7; + } else if (random.nextBoolean()) { + return false; +@@ -255,6 +333,7 @@ public class Bat extends AmbientCreature { + private static boolean isSpookySeason = false; + private static final int ONE_HOUR = 20 * 60 * 60; + private static int lastSpookyCheck = -ONE_HOUR; ++ public static boolean isHalloweenSeason(Level level) { return level.purpurConfig.forceHalloweenSeason || isHalloween(); } // Purpur + private static boolean isHalloween() { + if (net.minecraft.server.MinecraftServer.currentTick - lastSpookyCheck > ONE_HOUR) { + LocalDate localdate = LocalDate.now(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java +index 1f85f34c1e50f34fb270d2fac7d307c82a550bfa..324f52edd95b5f9a498e46def8c14435cfd00abb 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java +@@ -94,7 +94,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + @Override + protected void registerGoals() { + super.registerGoals(); +- this.goalSelector.addGoal(0, new PanicGoal(this, 1.25D)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 1.6D, 1.4D, EntitySelector.NO_SPECTATORS::test)); + this.goalSelector.addGoal(4, new AbstractFish.FishSwimGoal(this)); + } +@@ -107,7 +107,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + @Override + public void travel(Vec3 movementInput) { + if (this.isEffectiveAi() && this.isInWater()) { +- this.moveRelative(0.01F, movementInput); ++ this.moveRelative(getRider() != null ? getSpeed() : 0.01F, movementInput); // Purpur + this.move(MoverType.SELF, this.getDeltaMovement()); + this.setDeltaMovement(this.getDeltaMovement().scale(0.9D)); + if (this.getTarget() == null) { +@@ -166,7 +166,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + protected void playStepSound(BlockPos pos, BlockState state) { + } + +- static class FishMoveControl extends MoveControl { ++ static class FishMoveControl extends org.purpurmc.purpur.controller.WaterMoveControllerWASD { // Purpur + private final AbstractFish fish; + + FishMoveControl(AbstractFish owner) { +@@ -174,14 +174,22 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable { + this.fish = owner; + } + ++ // Purpur start + @Override +- public void tick() { ++ public void purpurTick(Player rider) { ++ super.purpurTick(rider); ++ fish.setDeltaMovement(fish.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ } ++ // Purpur end ++ ++ @Override ++ public void vanillaTick() { // Purpur + if (this.fish.isEyeInFluid(FluidTags.WATER)) { + this.fish.setDeltaMovement(this.fish.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); + } + + if (this.operation == MoveControl.Operation.MOVE_TO && !this.fish.getNavigation().isDone()) { +- float f = (float)(this.speedModifier * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f = (float)(this.getSpeedModifier() * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + this.fish.setSpeed(Mth.lerp(0.125F, this.fish.getSpeed(), f)); + double d = this.wantedX - this.fish.getX(); + double e = this.wantedY - this.fish.getY(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Animal.java b/src/main/java/net/minecraft/world/entity/animal/Animal.java +index 3c4d142e982c34a23bdb5da1f51c8dcacc0532c1..2ac88f06ebb79e515cd9934ac1e3e2c8003d9e3c 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Animal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Animal.java +@@ -39,6 +39,7 @@ public abstract class Animal extends AgeableMob { + @Nullable + public UUID loveCause; + public ItemStack breedItem; // CraftBukkit - Add breedItem variable ++ public abstract int getPurpurBreedTime(); // Purpur + + protected Animal(EntityType type, Level world) { + super(type, world); +@@ -150,7 +151,7 @@ public abstract class Animal extends AgeableMob { + if (this.isFood(itemstack)) { + int i = this.getAge(); + +- if (!this.level.isClientSide && i == 0 && this.canFallInLove()) { ++ if (!this.level.isClientSide && i == 0 && this.canFallInLove() && (this.level.purpurConfig.animalBreedingCooldownSeconds <= 0 || !this.level.hasBreedingCooldown(player.getUUID(), this.getClass()))) { // Purpur + this.usePlayerItem(player, hand, itemstack); + this.setInLove(player); + return InteractionResult.SUCCESS; +@@ -237,6 +238,14 @@ public abstract class Animal extends AgeableMob { + if (entityplayer == null && other.getLoveCause() != null) { + entityplayer = other.getLoveCause(); + } ++ // Purpur start ++ if (entityplayer != null && world.purpurConfig.animalBreedingCooldownSeconds > 0) { ++ if (world.hasBreedingCooldown(entityplayer.getUUID(), this.getClass())) { ++ return; ++ } ++ world.addBreedingCooldown(entityplayer.getUUID(), this.getClass()); ++ } ++ // Purpur end + // CraftBukkit start - call EntityBreedEvent + entityageable.setBaby(true); + entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F); +@@ -253,8 +262,10 @@ public abstract class Animal extends AgeableMob { + CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer, this, other, entityageable); + } + +- this.setAge(6000); +- other.setAge(6000); ++ // Purpur start ++ this.setAge(this.getPurpurBreedTime()); ++ other.setAge(other.getPurpurBreedTime()); ++ // Purpur end + this.resetLove(); + other.resetLove(); + world.addFreshEntityWithPassengers(entityageable, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason +diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java +index c33e5c51839c8e6ec04c1b302127d2bf0f48664c..d47dc0c3fe8c2b80d7b7eb828a12af6eb32145e4 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Bee.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java +@@ -43,6 +43,7 @@ import net.minecraft.world.entity.EntityType; + import net.minecraft.world.entity.LivingEntity; + import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.MobType; ++import net.minecraft.world.entity.MoverType; + import net.minecraft.world.entity.NeutralMob; + import net.minecraft.world.entity.PathfinderMob; + import net.minecraft.world.entity.Pose; +@@ -143,6 +144,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + public Bee(EntityType type, Level world) { + super(type, world); + this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(this.random, 20, 60); ++ final org.purpurmc.purpur.controller.FlyingMoveControllerWASD flyingController = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this, 0.25F, 1.0F, false); // Purpur + // Paper start - apply gravity to bees when they get stuck in the void, fixes MC-167279 + class BeeFlyingMoveControl extends FlyingMoveControl { + public BeeFlyingMoveControl(final Mob entity, final int maxPitchChange, final boolean noGravity) { +@@ -151,22 +153,89 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + public void tick() { ++ // Purpur start ++ if (mob.getRider() != null && mob.isControllable()) { ++ flyingController.purpurTick(mob.getRider()); ++ return; ++ } ++ // Purpur end + if (this.mob.getY() <= Bee.this.level.getMinBuildHeight()) { + this.mob.setNoGravity(false); + } + super.tick(); + } ++ ++ // Purpur start ++ @Override ++ public boolean hasWanted() { ++ return mob.getRider() != null || !mob.isControllable() || super.hasWanted(); ++ } ++ // Purpur end + } + this.moveControl = new BeeFlyingMoveControl(this, 20, true); + // Paper end + this.lookControl = new Bee.BeeLookControl(this); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, -1.0F); +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + this.setPathfindingMalus(BlockPathTypes.WATER_BORDER, 16.0F); + this.setPathfindingMalus(BlockPathTypes.COCOA, -1.0F); + this.setPathfindingMalus(BlockPathTypes.FENCE, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.beeRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.beeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.beeControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.beeMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(MoverType.SELF, mot.multiply(speed, speed, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.beeMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.beeBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.beeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.beeAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -181,6 +250,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new Bee.BeeAttackGoal(this, 1.399999976158142D, true)); + this.goalSelector.addGoal(1, new Bee.BeeEnterHiveGoal()); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); +@@ -196,6 +266,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.goalSelector.addGoal(7, new Bee.BeeGrowCropGoal()); + this.goalSelector.addGoal(8, new Bee.BeeWanderGoal()); + this.goalSelector.addGoal(9, new FloatGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new Bee.BeeHurtByOtherGoal(this)).setAlertOthers(new Class[0])); + this.targetSelector.addGoal(2, new Bee.BeeBecomeAngryTargetGoal(this)); + this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true)); +@@ -344,7 +415,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + boolean wantsToEnterHive() { + if (this.stayOutOfHiveCountdown <= 0 && !this.beePollinateGoal.isPollinating() && !this.hasStung() && this.getTarget() == null) { +- boolean flag = this.isTiredOfLookingForNectar() || this.level.isRaining() || this.level.isNight() || this.hasNectar(); ++ boolean flag = this.isTiredOfLookingForNectar() || (this.level.isRaining() && !this.level.purpurConfig.beeCanWorkInRain) || (this.level.isNight() && !this.level.purpurConfig.beeCanWorkAtNight) || this.hasNectar(); // Purpur + + return flag && !this.isHiveNearFire(); + } else { +@@ -384,6 +455,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.hurt(this.damageSources().drown(), 1.0F); + } + ++ if (flag && !this.level.purpurConfig.beeDiesAfterSting) setHasStung(false); else // Purpur + if (flag) { + ++this.timeSinceSting; + if (this.timeSinceSting % 5 == 0 && this.random.nextInt(Mth.clamp(1200 - this.timeSinceSting, 1, 1200)) == 0) { +@@ -732,6 +804,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + if (optional.isPresent()) { + Bee.this.savedFlowerPos = (BlockPos) optional.get(); + Bee.this.navigation.moveTo((double) Bee.this.savedFlowerPos.getX() + 0.5D, (double) Bee.this.savedFlowerPos.getY() + 0.5D, (double) Bee.this.savedFlowerPos.getZ() + 0.5D, 1.2000000476837158D); ++ new org.purpurmc.purpur.event.entity.BeeFoundFlowerEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(Bee.this.level, Bee.this.savedFlowerPos)).callEvent(); // Purpur + return true; + } else { + Bee.this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(Bee.this.random, 20, 60); +@@ -788,6 +861,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.pollinating = false; + Bee.this.navigation.stop(); + Bee.this.remainingCooldownBeforeLocatingNewFlower = 200; ++ new org.purpurmc.purpur.event.entity.BeeStopPollinatingEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), Bee.this.savedFlowerPos == null ? null : io.papermc.paper.util.MCUtil.toLocation(Bee.this.level, Bee.this.savedFlowerPos), Bee.this.hasNectar()).callEvent(); // Purpur + } + + @Override +@@ -834,6 +908,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + this.setWantedPos(); + } + ++ if (this.successfulPollinatingTicks == 0) new org.purpurmc.purpur.event.entity.BeeStartedPollinatingEvent((org.bukkit.entity.Bee) Bee.this.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(Bee.this.level, Bee.this.savedFlowerPos)).callEvent(); // Purpur + ++this.successfulPollinatingTicks; + if (Bee.this.random.nextFloat() < 0.05F && this.successfulPollinatingTicks > this.lastSoundPlayedTick + 60) { + this.lastSoundPlayedTick = this.successfulPollinatingTicks; +@@ -878,16 +953,16 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + } + } + +- private class BeeLookControl extends LookControl { ++ private class BeeLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + BeeLookControl(Mob entity) { + super(entity); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (!Bee.this.isAngry()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java +index 72b30a5cdeb8a43702d9ab5f198311929761fad1..fe08d83a49efe5e1648cafc50e9184dbd0db2115 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java +@@ -97,6 +97,51 @@ public class Cat extends TamableAnimal implements VariantHolder { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.catRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.catRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.catControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setInSittingPose(false); ++ setLying(false); ++ setRelaxStateOne(false); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.catMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.catBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.catTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.catAlwaysDropExp; ++ } ++ // Purpur end ++ + public ResourceLocation getResourceLocation() { + return this.getVariant().texture(); + } +@@ -105,6 +150,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + protected void registerGoals() { + this.temptGoal = new Cat.CatTemptGoal(this, 0.6D, Cat.TEMPT_INGREDIENT, true); + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 1.5D)); + this.goalSelector.addGoal(2, new SitWhenOrderedToGoal(this)); + this.goalSelector.addGoal(3, new Cat.CatRelaxOnOwnerGoal(this)); +@@ -117,6 +163,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + this.goalSelector.addGoal(10, new BreedGoal(this, 0.8D)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 0.8D, 1.0000001E-5F)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 10.0F)); ++ this.targetSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(this, Rabbit.class, false, (Predicate) null)); + this.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +@@ -308,6 +355,14 @@ public class Cat extends TamableAnimal implements VariantHolder { + return Mth.lerp(tickDelta, this.relaxStateOneAmountO, this.relaxStateOneAmount); + } + ++ // Purpur start ++ @Override ++ public void tame(Player player) { ++ setCollarColor(level.purpurConfig.catDefaultCollarColor); ++ super.tame(player); ++ } ++ // Purpur end ++ + @Nullable + @Override + public Cat getBreedOffspring(ServerLevel world, AgeableMob entity) { +@@ -373,6 +428,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + + @Override + public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (getRider() != null) return InteractionResult.PASS; // Purpur + ItemStack itemstack = player.getItemInHand(hand); + Item item = itemstack.getItem(); + +@@ -419,7 +475,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + } + } else if (this.isFood(itemstack)) { + this.usePlayerItem(player, hand, itemstack); +- if (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit ++ if ((this.level.purpurConfig.alwaysTameInCreative && player.getAbilities().instabuild) || (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled())) { // CraftBukkit // Purpur + this.tame(player); + this.setOrderedToSit(true); + this.level.broadcastEntityEvent(this, (byte) 7); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Chicken.java b/src/main/java/net/minecraft/world/entity/animal/Chicken.java +index b4dc621cb1be7cb4bf1cb31f921d4e9f6cffef88..c5e81244331d76535028f8296d10939933010d09 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Chicken.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Chicken.java +@@ -54,16 +54,65 @@ public class Chicken extends Animal { + this.setPathfindingMalus(BlockPathTypes.WATER, 0.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.chickenRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.chickenRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.chickenControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.chickenMaxHealth); ++ if (level.purpurConfig.chickenRetaliate) { ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(2.0D); ++ } ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.chickenBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.chickenTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.chickenAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); +- this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ // this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D)); // Purpur - moved down + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new TemptGoal(this, 1.0D, Chicken.FOOD_ITEMS, false)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.1D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ // Purpur start ++ if (level.purpurConfig.chickenRetaliate) { ++ this.goalSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.MeleeAttackGoal(this, 1.0D, false)); ++ this.targetSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal(this)); ++ } else { ++ this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D)); ++ } ++ // Purpur end + } + + @Override +@@ -72,7 +121,7 @@ public class Chicken extends Animal { + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 4.0D).add(Attributes.MOVEMENT_SPEED, 0.25D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 4.0D).add(Attributes.MOVEMENT_SPEED, 0.25D).add(Attributes.ATTACK_DAMAGE, 0.0D); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cod.java b/src/main/java/net/minecraft/world/entity/animal/Cod.java +index 824e5e4fe7619ae46061c3c978c9a044db8c84ab..2a45b487e5305e7c40cc8de4ddbb142af4b041de 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cod.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cod.java +@@ -13,6 +13,33 @@ public class Cod extends AbstractSchoolingFish { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.codRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.codControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.codMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.codTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.codAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public ItemStack getBucketItemStack() { + return new ItemStack(Items.COD_BUCKET); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cow.java b/src/main/java/net/minecraft/world/entity/animal/Cow.java +index abae850f5babfd75c7547e88fb7637e9775991d3..54d9213d9de26a14a5ca770440d098bf0438373e 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cow.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cow.java +@@ -2,6 +2,7 @@ package net.minecraft.world.entity.animal; + + import javax.annotation.Nullable; + import net.minecraft.core.BlockPos; ++import net.minecraft.core.particles.ParticleTypes; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.sounds.SoundEvent; + import net.minecraft.sounds.SoundEvents; +@@ -29,6 +30,7 @@ import net.minecraft.world.item.ItemUtils; + import net.minecraft.world.item.Items; + import net.minecraft.world.item.crafting.Ingredient; + import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; + // CraftBukkit start + import org.bukkit.craftbukkit.event.CraftEventFactory; +@@ -36,25 +38,74 @@ import org.bukkit.craftbukkit.inventory.CraftItemStack; + // CraftBukkit end + + public class Cow extends Animal { ++ private boolean isNaturallyAggressiveToPlayers; // Purpur + + public Cow(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.cowRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.cowRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.cowControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.cowMaxHealth); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(this.level.purpurConfig.cowNaturallyAggressiveToPlayersDamage); // Purpur ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.cowBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.cowTakeDamageFromWater; ++ } ++ ++ @Override ++ public net.minecraft.world.entity.SpawnGroupData finalizeSpawn(net.minecraft.world.level.ServerLevelAccessor world, net.minecraft.world.DifficultyInstance difficulty, net.minecraft.world.entity.MobSpawnType spawnReason, net.minecraft.world.entity.SpawnGroupData entityData, net.minecraft.nbt.CompoundTag entityNbt) { ++ this.isNaturallyAggressiveToPlayers = world.getLevel().purpurConfig.cowNaturallyAggressiveToPlayersChance > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.cowNaturallyAggressiveToPlayersChance; ++ return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.cowAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 2.0D)); ++ this.goalSelector.addGoal(1, new net.minecraft.world.entity.ai.goal.MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); ++ if (level.purpurConfig.cowFeedMushrooms > 0) this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, Ingredient.of(Items.WHEAT, Blocks.RED_MUSHROOM.asItem(), Blocks.BROWN_MUSHROOM.asItem()), false)); else // Purpur + this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, Ingredient.of(Items.WHEAT), false)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, target -> isNaturallyAggressiveToPlayers)); // Purpur + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.MOVEMENT_SPEED, 0.20000000298023224D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.MOVEMENT_SPEED, 0.20000000298023224D).add(Attributes.ATTACK_DAMAGE, 0.0D); // Purpur + } + + @Override +@@ -84,6 +135,7 @@ public class Cow extends Animal { + + @Override + public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (getRider() != null) return InteractionResult.PASS; // Purpur + ItemStack itemstack = player.getItemInHand(hand); + + if (itemstack.is(Items.BUCKET) && !this.isBaby()) { +@@ -91,7 +143,7 @@ public class Cow extends Animal { + org.bukkit.event.player.PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level, player, this.blockPosition(), this.blockPosition(), null, itemstack, Items.MILK_BUCKET, hand); + + if (event.isCancelled()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + // CraftBukkit end + +@@ -100,6 +152,10 @@ public class Cow extends Animal { + + player.setItemInHand(hand, itemstack1); + return InteractionResult.sidedSuccess(this.level.isClientSide); ++ // Purpur start - feed mushroom to change to mooshroom ++ } else if (level.purpurConfig.cowFeedMushrooms > 0 && this.getType() != EntityType.MOOSHROOM && isMushroom(itemstack)) { ++ return this.feedMushroom(player, itemstack); ++ // Purpur end + } else { + return super.mobInteract(player, hand); + } +@@ -115,4 +171,69 @@ public class Cow extends Animal { + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { + return this.isBaby() ? dimensions.height * 0.95F : 1.3F; + } ++ ++ // Purpur start - feed mushroom to change to mooshroom ++ private int redMushroomsFed = 0; ++ private int brownMushroomsFed = 0; ++ ++ private boolean isMushroom(ItemStack stack) { ++ return stack.getItem() == Blocks.RED_MUSHROOM.asItem() || stack.getItem() == Blocks.BROWN_MUSHROOM.asItem(); ++ } ++ ++ private int incrementFeedCount(ItemStack stack) { ++ if (stack.getItem() == Blocks.RED_MUSHROOM.asItem()) { ++ return ++redMushroomsFed; ++ } else { ++ return ++brownMushroomsFed; ++ } ++ } ++ ++ private InteractionResult feedMushroom(Player player, ItemStack stack) { ++ level.broadcastEntityEvent(this, (byte) 18); // hearts ++ playSound(SoundEvents.COW_MILK, 1.0F, 1.0F); ++ if (incrementFeedCount(stack) < level.purpurConfig.cowFeedMushrooms) { ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ return InteractionResult.CONSUME; // require 5 mushrooms to transform (prevents mushroom duping) ++ } ++ MushroomCow mooshroom = EntityType.MOOSHROOM.create(level); ++ if (mooshroom == null) { ++ return InteractionResult.PASS; ++ } ++ if (stack.getItem() == Blocks.BROWN_MUSHROOM.asItem()) { ++ mooshroom.setVariant(MushroomCow.MushroomType.BROWN); ++ } else { ++ mooshroom.setVariant(MushroomCow.MushroomType.RED); ++ } ++ mooshroom.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); ++ mooshroom.setHealth(this.getHealth()); ++ mooshroom.setAge(getAge()); ++ mooshroom.copyPosition(this); ++ mooshroom.setYBodyRot(this.yBodyRot); ++ mooshroom.setYHeadRot(this.getYHeadRot()); ++ mooshroom.yRotO = this.yRotO; ++ mooshroom.xRotO = this.xRotO; ++ if (this.hasCustomName()) { ++ mooshroom.setCustomName(this.getCustomName()); ++ } ++ if (CraftEventFactory.callEntityTransformEvent(this, mooshroom, org.bukkit.event.entity.EntityTransformEvent.TransformReason.INFECTION).isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ if (!new com.destroystokyo.paper.event.entity.EntityTransformedEvent(this.getBukkitEntity(), mooshroom.getBukkitEntity(), com.destroystokyo.paper.event.entity.EntityTransformedEvent.TransformedReason.INFECTED).callEvent()) { ++ return InteractionResult.PASS; ++ } ++ this.level.addFreshEntity(mooshroom); ++ this.remove(RemovalReason.DISCARDED); ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ for (int i = 0; i < 15; ++i) { ++ ((ServerLevel) level).sendParticles(((ServerLevel) level).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 2), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java +index e93abb02744b5cd8db88e01b6ccf145498903b11..a077edbe97ce89e11a26fe3ebeb0bdd996593f78 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java +@@ -78,19 +78,104 @@ public class Dolphin extends WaterAnimal { + public static final Predicate ALLOWED_ITEMS = (entityitem) -> { + return !entityitem.hasPickUpDelay() && entityitem.isAlive() && entityitem.isInWater(); + }; ++ private int spitCooldown; // Purpur ++ private boolean isNaturallyAggressiveToPlayers; // Purpur + + public Dolphin(EntityType type, Level world) { + super(type, world); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ class DolphinMoveControl extends SmoothSwimmingMoveControl { ++ private final org.purpurmc.purpur.controller.WaterMoveControllerWASD waterMoveControllerWASD; ++ private final Dolphin dolphin; ++ ++ public DolphinMoveControl(Dolphin dolphin, int pitchChange, int yawChange, float speedInWater, float speedInAir, boolean buoyant) { ++ super(dolphin, pitchChange, yawChange, speedInWater, speedInAir, buoyant); ++ this.dolphin = dolphin; ++ this.waterMoveControllerWASD = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(dolphin); ++ } ++ ++ @Override ++ public void tick() { ++ if (dolphin.getRider() != null && dolphin.isControllable()) { ++ purpurTick(dolphin.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ ++ public void purpurTick(Player rider) { ++ if (dolphin.getAirSupply() < 150) { ++ // if drowning override player WASD controls to find air ++ super.tick(); ++ } else { ++ waterMoveControllerWASD.purpurTick(rider); ++ dolphin.setDeltaMovement(dolphin.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ } ++ } ++ }; ++ this.moveControl = new DolphinMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur end + this.lookControl = new SmoothSwimmingLookControl(this, 10); + this.setCanPickUpLoot(true); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.dolphinRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.dolphinControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (spitCooldown == 0 && getRider() != null) { ++ spitCooldown = level.purpurConfig.dolphinSpitCooldown; ++ ++ org.bukkit.craftbukkit.entity.CraftPlayer player = (org.bukkit.craftbukkit.entity.CraftPlayer) getRider().getBukkitEntity(); ++ if (!player.hasPermission("allow.special.dolphin")) { ++ return false; ++ } ++ ++ org.bukkit.Location loc = player.getEyeLocation(); ++ loc.setPitch(loc.getPitch() - 10); ++ org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(10).add(loc.toVector()); ++ ++ org.purpurmc.purpur.entity.DolphinSpit spit = new org.purpurmc.purpur.entity.DolphinSpit(level, this); ++ spit.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), level.purpurConfig.dolphinSpitSpeed, 5.0F); ++ ++ level.addFreshEntity(spit); ++ playSound(SoundEvents.DOLPHIN_ATTACK, 1.0F, 1.0F + (random.nextFloat() - random.nextFloat()) * 0.2F); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.dolphinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.dolphinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.dolphinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Nullable + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { + this.setAirSupply(this.getMaxAirSupply()); + this.setXRot(0.0F); ++ this.isNaturallyAggressiveToPlayers = world.getLevel().purpurConfig.dolphinNaturallyAggressiveToPlayersChance > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.dolphinNaturallyAggressiveToPlayersChance; // Purpur + return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); + } + +@@ -160,17 +245,21 @@ public class Dolphin extends WaterAnimal { + protected void registerGoals() { + this.goalSelector.addGoal(0, new BreathAirGoal(this)); + this.goalSelector.addGoal(0, new TryFindWaterGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur + this.goalSelector.addGoal(1, new Dolphin.DolphinSwimToTreasureGoal(this)); + this.goalSelector.addGoal(2, new Dolphin.DolphinSwimWithPlayerGoal(this, 4.0D)); + this.goalSelector.addGoal(4, new RandomSwimmingGoal(this, 1.0D, 10)); + this.goalSelector.addGoal(4, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(5, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(5, new DolphinJumpGoal(this, 10)); +- this.goalSelector.addGoal(6, new MeleeAttackGoal(this, 1.2000000476837158D, true)); ++ //this.goalSelector.addGoal(6, new MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur - moved up + this.goalSelector.addGoal(8, new Dolphin.PlayWithItemsGoal()); + this.goalSelector.addGoal(8, new FollowBoatGoal(this)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, Guardian.class, 8.0F, 1.0D, 1.0D)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Guardian.class})).setAlertOthers()); ++ this.targetSelector.addGoal(2, new net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, target -> isNaturallyAggressiveToPlayers)); // Purpur + } + + public static AttributeSupplier.Builder createAttributes() { +@@ -221,7 +310,7 @@ public class Dolphin extends WaterAnimal { + + @Override + protected boolean canRide(Entity entity) { +- return true; ++ return boardingCooldown <= 0; // Purpur - make dolphin honor ride cooldown like all other non-boss mobs; + } + + @Override +@@ -256,6 +345,11 @@ public class Dolphin extends WaterAnimal { + @Override + public void tick() { + super.tick(); ++ // Purpur start ++ if (spitCooldown > 0) { ++ spitCooldown--; ++ } ++ // Purpur end + if (this.isNoAi()) { + this.setAirSupply(this.getMaxAirSupply()); + } else { +@@ -401,6 +495,7 @@ public class Dolphin extends WaterAnimal { + + @Override + public boolean canUse() { ++ if (this.dolphin.level.purpurConfig.dolphinDisableTreasureSearching) return false; // Purpur + return this.dolphin.gotFish() && this.dolphin.getAirSupply() >= 100 && this.dolphin.level.getWorld().canGenerateStructures(); // MC-151364, SPIGOT-5494: hangs if generate-structures=false + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Fox.java b/src/main/java/net/minecraft/world/entity/animal/Fox.java +index 89894bc6a55bc7e456a9d49ac48f6a8192b890ae..f0eb5e0c01923f884b1c7c48e8d67ed5cd429854 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Fox.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Fox.java +@@ -35,6 +35,7 @@ import net.minecraft.util.RandomSource; + import net.minecraft.util.StringRepresentable; + import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; + import net.minecraft.world.entity.AgeableMob; + import net.minecraft.world.entity.Entity; +@@ -88,6 +89,7 @@ import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.CaveVines; + import net.minecraft.world.level.block.SweetBerryBushBlock; + import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.gameevent.GameEvent; + import net.minecraft.world.level.pathfinder.BlockPathTypes; + import net.minecraft.world.phys.Vec3; + +@@ -141,6 +143,64 @@ public class Fox extends Animal implements VariantHolder { + this.setCanPickUpLoot(true); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.foxRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.foxRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.foxControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return getRider() != null && this.isControllable() ? 0.5F : super.getJumpPower(); ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setCanPickUpLoot(false); ++ clearStates(); ++ setIsPouncing(false); ++ spitOutItem(getItemBySlot(EquipmentSlot.MAINHAND)); ++ setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY); ++ } ++ ++ @Override ++ public void onDismount(Player rider) { ++ super.onDismount(rider); ++ setCanPickUpLoot(true); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.foxMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.foxBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.foxTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.foxAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -160,6 +220,7 @@ public class Fox extends Animal implements VariantHolder { + return entityliving instanceof AbstractSchoolingFish; + }); + this.goalSelector.addGoal(0, new Fox.FoxFloatGoal()); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new ClimbOnTopOfPowderSnowGoal(this, this.level)); + this.goalSelector.addGoal(1, new Fox.FaceplantGoal()); + this.goalSelector.addGoal(2, new Fox.FoxPanicGoal(2.2D)); +@@ -186,6 +247,7 @@ public class Fox extends Animal implements VariantHolder { + this.goalSelector.addGoal(11, new Fox.FoxSearchForItemsGoal()); + this.goalSelector.addGoal(12, new Fox.FoxLookAtPlayerGoal(this, Player.class, 24.0F)); + this.goalSelector.addGoal(13, new Fox.PerchAndSearchGoal()); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(3, new Fox.DefendTrustedTargetGoal(LivingEntity.class, false, false, (entityliving) -> { + return Fox.TRUSTED_TARGET_SELECTOR.test(entityliving) && !this.trusts(entityliving.getUUID()); + })); +@@ -342,6 +404,11 @@ public class Fox extends Animal implements VariantHolder { + } + + private void setTargetGoals() { ++ // Purpur start - do not add duplicate goals ++ this.targetSelector.removeGoal(this.landTargetGoal); ++ this.targetSelector.removeGoal(this.turtleEggTargetGoal); ++ this.targetSelector.removeGoal(this.fishTargetGoal); ++ // Purpur end + if (this.getVariant() == Fox.Type.RED) { + this.targetSelector.addGoal(4, this.landTargetGoal); + this.targetSelector.addGoal(4, this.turtleEggTargetGoal); +@@ -375,6 +442,7 @@ public class Fox extends Animal implements VariantHolder { + + public void setVariant(Fox.Type variant) { + this.entityData.set(Fox.DATA_TYPE_ID, variant.getId()); ++ this.setTargetGoals(); // Purpur - fix API bug not updating pathfinders on type change + } + + List getTrustedUUIDs() { +@@ -711,6 +779,29 @@ public class Fox extends Animal implements VariantHolder { + return this.getTrustedUUIDs().contains(uuid); + } + ++ // Purpur start ++ @Override ++ public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ if (level.purpurConfig.foxTypeChangesWithTulips) { ++ ItemStack itemstack = player.getItemInHand(hand); ++ if (getVariant() == Type.RED && itemstack.getItem() == Items.WHITE_TULIP) { ++ setVariant(Type.SNOW); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ } else if (getVariant() == Type.SNOW && itemstack.getItem() == Items.ORANGE_TULIP) { ++ setVariant(Type.RED); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ } ++ return super.mobInteract(player, hand); ++ } ++ // Purpur end ++ + @Override + // Paper start - Cancellable death event + protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(DamageSource source) { +@@ -758,16 +849,16 @@ public class Fox extends Animal implements VariantHolder { + return new Vec3(0.0D, (double) (0.55F * this.getEyeHeight()), (double) (this.getBbWidth() * 0.4F)); + } + +- public class FoxLookControl extends LookControl { ++ public class FoxLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + public FoxLookControl() { + super(Fox.this); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (!Fox.this.isSleeping()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +@@ -778,16 +869,16 @@ public class Fox extends Animal implements VariantHolder { + } + } + +- private class FoxMoveControl extends MoveControl { ++ private class FoxMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + public FoxMoveControl() { + super(Fox.this); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (Fox.this.canMove()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +@@ -905,8 +996,10 @@ public class Fox extends Animal implements VariantHolder { + CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer2, this.animal, this.partner, entityfox); + } + +- this.animal.setAge(6000); +- this.partner.setAge(6000); ++ // Purpur start ++ this.animal.setAge(this.animal.getPurpurBreedTime()); ++ this.partner.setAge(this.partner.getPurpurBreedTime()); ++ // Purpur end + this.animal.resetLove(); + this.partner.resetLove(); + worldserver.addFreshEntityWithPassengers(entityfox, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason +@@ -1294,7 +1387,7 @@ public class Fox extends Animal implements VariantHolder { + } + + protected void onReachedTarget() { +- if (Fox.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (Fox.this.level.purpurConfig.foxBypassMobGriefing || Fox.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + BlockState iblockdata = Fox.this.level.getBlockState(this.blockPos); + + if (iblockdata.is(Blocks.SWEET_BERRY_BUSH)) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +index 4fbbd74cda7e4f2c623db46c2c94d9697ca5df05..b5f445750a5ccbe7658396bdcc9648dc41f39ced 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +@@ -63,14 +63,59 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + private int remainingPersistentAngerTime; + @Nullable + private UUID persistentAngerTarget; ++ @Nullable private UUID summoner; // Purpur + + public IronGolem(EntityType type, Level world) { + super(type, world); + this.setMaxUpStep(1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.ironGolemRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.ironGolemRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.ironGolemControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.ironGolemMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.ironGolemTakeDamageFromWater; ++ } ++ ++ @Nullable ++ public UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.ironGolemAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ if (level.purpurConfig.ironGolemCanSwim) this.goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.FloatGoal(this)); // Purpur ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ if (this.level.purpurConfig.ironGolemPoppyCalm) this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.ReceiveFlower(this)); // Purpur + this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(2, new MoveTowardsTargetGoal(this, 0.9D, 32.0F)); + this.goalSelector.addGoal(2, new MoveBackToVillageGoal(this, 0.6D, false)); +@@ -78,6 +123,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + this.goalSelector.addGoal(5, new OfferFlowerGoal(this)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new DefendVillageTargetGoal(this)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +@@ -148,6 +194,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putBoolean("PlayerCreated", this.isPlayerCreated()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + this.addPersistentAngerSaveData(nbt); + } + +@@ -155,6 +202,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.setPlayerCreated(nbt.getBoolean("PlayerCreated")); ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + this.readPersistentAngerSaveData(this.level, nbt); + } + +@@ -279,13 +327,13 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + ItemStack itemstack = player.getItemInHand(hand); + + if (!itemstack.is(Items.IRON_INGOT)) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else { + float f = this.getHealth(); + + this.heal(25.0F); + if (this.getHealth() == f) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else { + float f1 = 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F; + +@@ -294,6 +342,8 @@ public class IronGolem extends AbstractGolem implements NeutralMob { + itemstack.shrink(1); + } + ++ if (this.level.purpurConfig.ironGolemHealCalm && isAngry() && getHealth() == getMaxHealth()) stopBeingAngry(); // Purpur ++ + return InteractionResult.sidedSuccess(this.level.isClientSide); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +index 68a5ee85e64802e4509ba0d184fc0ceb3cbe2d11..b5d0d3aaa0442b4753aef8fdf8f85f017e1dd811 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java ++++ b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +@@ -63,6 +63,43 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder { +@@ -145,7 +182,7 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder> optional = this.getEffectFromItemStack(itemstack); + + if (!optional.isPresent()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + Pair pair = (Pair) optional.get(); +@@ -170,7 +207,7 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder(this, Chicken.class, false)); + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, false, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Panda.java b/src/main/java/net/minecraft/world/entity/animal/Panda.java +index 9c1e02c3a990cd0f8bba1c84c170b438278c02a7..d1e45052fc96b6f81a331c6c73cb68ff96238359 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Panda.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Panda.java +@@ -108,6 +108,53 @@ public class Panda extends Animal { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.pandaRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.pandaRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.pandaControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setForwardMot(0.0F); ++ sit(false); ++ eat(false); ++ setOnBack(false); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.pandaMaxHealth); ++ setAttributes(); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.pandaBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.pandaTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.pandaAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean canTakeItem(ItemStack stack) { + EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(stack); +@@ -269,6 +316,7 @@ public class Panda extends Animal { + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new Panda.PandaPanicGoal(this, 2.0D)); + this.goalSelector.addGoal(2, new Panda.PandaBreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new Panda.PandaAttackGoal(this, 1.2000000476837158D, true)); +@@ -284,6 +332,7 @@ public class Panda extends Animal { + this.goalSelector.addGoal(12, new Panda.PandaRollGoal(this)); + this.goalSelector.addGoal(13, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(14, new WaterAvoidingRandomStrollGoal(this, 1.0D)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new Panda.PandaHurtByTargetGoal(this, new Class[0])).setAlertOthers(new Class[0])); + } + +@@ -607,7 +656,10 @@ public class Panda extends Animal { + + public void setAttributes() { + if (this.isWeak()) { +- this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(10.0D); ++ // Purpur start ++ net.minecraft.world.entity.ai.attributes.AttributeInstance maxHealth = this.getAttribute(Attributes.MAX_HEALTH); ++ maxHealth.setBaseValue(maxHealth.getValue() / 2); ++ // Purpur end + } + + if (this.isLazy()) { +@@ -630,7 +682,7 @@ public class Panda extends Animal { + ItemStack itemstack = player.getItemInHand(hand); + + if (this.isScared()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } else if (this.isOnBack()) { + this.setOnBack(false); + return InteractionResult.sidedSuccess(this.level.isClientSide); +@@ -647,7 +699,7 @@ public class Panda extends Animal { + this.setInLove(player); + } else { + if (this.level.isClientSide || this.isSitting() || this.isInWater()) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + + this.tryToSit(); +@@ -666,7 +718,7 @@ public class Panda extends Animal { + + return InteractionResult.SUCCESS; + } else { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + } + +@@ -706,7 +758,7 @@ public class Panda extends Animal { + return !this.isOnBack() && !this.isScared() && !this.isEating() && !this.isRolling() && !this.isSitting(); + } + +- private static class PandaMoveControl extends MoveControl { ++ private static class PandaMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Panda panda; + +@@ -716,9 +768,9 @@ public class Panda extends Animal { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.panda.canPerformAction()) { +- super.tick(); ++ super.vanillaTick(); // Purpur + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Parrot.java b/src/main/java/net/minecraft/world/entity/animal/Parrot.java +index e6e40770acf71b9079e8f6ac07025319dd8e2e4e..8ca75f748ac7dcf872b5677648ba384992242a07 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Parrot.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Parrot.java +@@ -129,12 +129,88 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, Level world) { + super(type, world); +- this.moveControl = new FlyingMoveControl(this, 10, false); ++ // Purpur start ++ final org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD flyingController = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F); ++ class ParrotMoveControl extends FlyingMoveControl { ++ public ParrotMoveControl(Mob entity, int maxPitchChange, boolean noGravity) { ++ super(entity, maxPitchChange, noGravity); ++ } ++ ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ flyingController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ ++ @Override ++ public boolean hasWanted() { ++ return mob.getRider() != null && mob.isControllable() ? getForwardMot() != 0 || getStrafeMot() != 0 : super.hasWanted(); ++ } ++ } ++ this.moveControl = new ParrotMoveControl(this, 10, false); ++ // Purpur end + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, -1.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, -1.0F); + this.setPathfindingMalus(BlockPathTypes.COCOA, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.parrotRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.parrotRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.parrotControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.parrotMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.25, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.parrotMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return 6000; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.parrotTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.parrotAlwaysDropExp; ++ } ++ // Purpur end ++ + @Nullable + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { +@@ -153,8 +229,11 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, LevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { +@@ -306,13 +386,13 @@ public class Parrot extends ShoulderRidingEntity implements VariantHolder type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.polarBearRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.polarBearRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.polarBearControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (!isStanding()) { ++ if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0) { ++ setStanding(true); ++ playSound(SoundEvents.POLAR_BEAR_WARNING, 1.0F, 1.0F); ++ } ++ } ++ return false; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.polarBearMaxHealth); ++ } ++ ++ public boolean canMate(Animal other) { ++ if (other == this) { ++ return false; ++ } else if (this.isStanding()) { ++ return false; ++ } else if (this.getTarget() != null) { ++ return false; ++ } else if (!(other instanceof PolarBear)) { ++ return false; ++ } else { ++ PolarBear bear = (PolarBear) other; ++ if (bear.isStanding()) { ++ return false; ++ } ++ if (bear.getTarget() != null) { ++ return false; ++ } ++ return this.isInLove() && bear.isInLove(); ++ } ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.polarBearBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.polarBearTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.polarBearAlwaysDropExp; ++ } ++ // Purpur end ++ + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel world, AgeableMob entity) { +@@ -73,19 +143,27 @@ public class PolarBear extends Animal implements NeutralMob { + + @Override + public boolean isFood(ItemStack stack) { +- return false; ++ return level.purpurConfig.polarBearBreedableItem != null && stack.getItem() == level.purpurConfig.polarBearBreedableItem; // Purpur + } + + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PolarBear.PolarBearMeleeAttackGoal()); + this.goalSelector.addGoal(1, new PolarBear.PolarBearPanicGoal()); ++ // Purpur start ++ if (level.purpurConfig.polarBearBreedableItem != null) { ++ this.goalSelector.addGoal(2, new net.minecraft.world.entity.ai.goal.BreedGoal(this, 1.0D)); ++ this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, net.minecraft.world.item.crafting.Ingredient.of(level.purpurConfig.polarBearBreedableItem), false)); ++ } ++ // Purpur end + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D)); + this.goalSelector.addGoal(5, new RandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new PolarBear.PolarBearHurtByTargetGoal()); + this.targetSelector.addGoal(2, new PolarBear.PolarBearAttackPlayersGoal()); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +@@ -202,6 +280,11 @@ public class PolarBear extends Animal implements NeutralMob { + this.updatePersistentAnger((ServerLevel)this.level, true); + } + ++ // Purpur start ++ if (isStanding() && --standTimer <= 0) { ++ setStanding(false); ++ } ++ // Purpur end + } + + @Override +@@ -231,6 +314,7 @@ public class PolarBear extends Animal implements NeutralMob { + + public void setStanding(boolean warning) { + this.entityData.set(DATA_STANDING_ID, warning); ++ standTimer = warning ? 20 : -1; // Purpur + } + + public float getStandingAnimationScale(float tickDelta) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java +index 9aa5aa0d66257bf1413a904c516293aea30d2ca8..d152c50f17e2ab7a37b0c295c7f62e63889b8b76 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java +@@ -45,6 +45,33 @@ public class Pufferfish extends AbstractFish { + this.refreshDimensions(); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.pufferfishRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.pufferfishControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.pufferfishMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.pufferfishTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.pufferfishAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java +index 2e75066db6cf4903f04428b73c4e868988776920..3395bc1d9140ab5496ad998343a963ae12f630d6 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java +@@ -83,6 +83,7 @@ public class Rabbit extends Animal implements VariantHolder { + private boolean wasOnGround; + private int jumpDelayTicks; + public int moreCarrotTicks; ++ private boolean actualJump; // Purpur + + public Rabbit(EntityType type, Level world) { + super(type, world); +@@ -91,6 +92,71 @@ public class Rabbit extends Animal implements VariantHolder { + this.initializePathFinderGoals(); // CraftBukkit - moved code + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.rabbitRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.rabbitRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.rabbitControllable; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (onGround) { ++ actualJump = true; ++ jumpFromGround(); ++ actualJump = false; ++ } ++ return true; ++ } ++ ++ private void handleJumping() { ++ if (onGround) { ++ RabbitJumpControl jumpController = (RabbitJumpControl) jumpControl; ++ if (!wasOnGround) { ++ setJumping(false); ++ jumpController.setCanJump(false); ++ } ++ if (!jumpController.wantJump()) { ++ if (moveControl.hasWanted()) { ++ startJumping(); ++ } ++ } else if (!jumpController.canJump()) { ++ jumpController.setCanJump(true); ++ } ++ } ++ wasOnGround = onGround; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.rabbitMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.rabbitBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.rabbitTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.rabbitAlwaysDropExp; ++ } ++ // Purpur end ++ + // CraftBukkit start - code from constructor + public void initializePathFinderGoals(){ + this.setSpeedModifier(0.0D); +@@ -100,6 +166,7 @@ public class Rabbit extends Animal implements VariantHolder { + @Override + public void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new ClimbOnTopOfPowderSnowGoal(this, this.level)); + this.goalSelector.addGoal(1, new Rabbit.RabbitPanicGoal(this, 2.2D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 0.8D)); +@@ -114,6 +181,13 @@ public class Rabbit extends Animal implements VariantHolder { + + @Override + protected float getJumpPower() { ++ if (getRider() != null && this.isControllable()) { ++ if (getForwardMot() < 0) { ++ setSpeed(getForwardMot() * 2F); ++ } ++ return actualJump ? 0.5F : 0.3F; ++ } ++ // Purpur end + if (!this.horizontalCollision && (!this.moveControl.hasWanted() || this.moveControl.getWantedY() <= this.getY() + 0.5D)) { + Path pathentity = this.navigation.getPath(); + +@@ -132,7 +206,7 @@ public class Rabbit extends Animal implements VariantHolder { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + super.jumpFromGround(); + double d0 = this.moveControl.getSpeedModifier(); + +@@ -182,6 +256,13 @@ public class Rabbit extends Animal implements VariantHolder { + + @Override + public void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ handleJumping(); ++ return; ++ } ++ // Purpur end ++ + if (this.jumpDelayTicks > 0) { + --this.jumpDelayTicks; + } +@@ -399,10 +480,23 @@ public class Rabbit extends Animal implements VariantHolder { + } + + this.setVariant(entityrabbit_variant); ++ ++ // Purpur start ++ if (entityrabbit_variant != Variant.EVIL && world.getLevel().purpurConfig.rabbitNaturalToast > 0D && random.nextDouble() <= world.getLevel().purpurConfig.rabbitNaturalToast) { ++ setCustomName(Component.translatable("Toast")); ++ } ++ // Purpur end ++ + return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData, entityNbt); + } + + private static Rabbit.Variant getRandomRabbitVariant(LevelAccessor world, BlockPos pos) { ++ // Purpur start ++ Level level = world.getMinecraftWorld(); ++ if (level.purpurConfig.rabbitNaturalKiller > 0D && world.getRandom().nextDouble() <= level.purpurConfig.rabbitNaturalKiller) { ++ return Rabbit.Variant.EVIL; ++ } ++ // Purpur end + Holder holder = world.getBiome(pos); + int i = world.getRandom().nextInt(100); + +@@ -466,7 +560,7 @@ public class Rabbit extends Animal implements VariantHolder { + } + } + +- private static class RabbitMoveControl extends MoveControl { ++ private static class RabbitMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Rabbit rabbit; + private double nextJumpSpeed; +@@ -477,14 +571,14 @@ public class Rabbit extends Animal implements VariantHolder { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.rabbit.onGround && !this.rabbit.jumping && !((Rabbit.RabbitJumpControl) this.rabbit.jumpControl).wantJump()) { + this.rabbit.setSpeedModifier(0.0D); + } else if (this.hasWanted()) { + this.rabbit.setSpeedModifier(this.nextJumpSpeed); + } + +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + @Override +@@ -546,7 +640,7 @@ public class Rabbit extends Animal implements VariantHolder { + @Override + public boolean canUse() { + if (this.nextStartTick <= 0) { +- if (!this.rabbit.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.rabbit.level.purpurConfig.rabbitBypassMobGriefing && !this.rabbit.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Salmon.java b/src/main/java/net/minecraft/world/entity/animal/Salmon.java +index 0af79daa357f53a8871e293b57e16c099e5d3f64..e0da8d1974f88e1426034620f78a29f9bdb5adf4 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Salmon.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Salmon.java +@@ -13,6 +13,33 @@ public class Salmon extends AbstractSchoolingFish { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.salmonRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.salmonControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.salmonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.salmonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.salmonAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public int getMaxSchoolSize() { + return 5; +diff --git a/src/main/java/net/minecraft/world/entity/animal/Sheep.java b/src/main/java/net/minecraft/world/entity/animal/Sheep.java +index fb3777e158065a6ce306a2a6e66bec053da2aeb4..9399361c8d26a140fa6042988a30a1d3d552e418 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Sheep.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Sheep.java +@@ -116,10 +116,48 @@ public class Sheep extends Animal implements Shearable { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.sheepRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.sheepRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.sheepControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.sheepMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.sheepBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.sheepTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.sheepAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.eatBlockGoal = new EatBlockGoal(this); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 1.25D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new TemptGoal(this, 1.1D, Ingredient.of(Items.WHEAT), false)); +@@ -254,7 +292,7 @@ public class Sheep extends Animal implements Shearable { + return InteractionResult.PASS; + } + // CraftBukkit end +- this.shear(SoundSource.PLAYERS); ++ this.shear(SoundSource.PLAYERS, net.minecraft.world.item.enchantment.EnchantmentHelper.getMobLooting(player)); // Purpur + this.gameEvent(GameEvent.SHEAR, player); + itemstack.hurtAndBreak(1, player, (entityhuman1) -> { + entityhuman1.broadcastBreakEvent(hand); +@@ -269,14 +307,15 @@ public class Sheep extends Animal implements Shearable { + } + + @Override +- public void shear(SoundSource shearedSoundCategory) { ++ public void shear(SoundSource shearedSoundCategory, int looting) { // Purpur + this.level.playSound((Player) null, (Entity) this, SoundEvents.SHEEP_SHEAR, shearedSoundCategory, 1.0F, 1.0F); + this.setSheared(true); + int i = 1 + this.random.nextInt(3); ++ if (org.purpurmc.purpur.PurpurConfig.allowShearsLooting) i += looting; // Purpur + + for (int j = 0; j < i; ++j) { + this.forceDrops = true; // CraftBukkit +- ItemEntity entityitem = this.spawnAtLocation((ItemLike) Sheep.ITEM_BY_DYE.get(this.getColor()), 1); ++ ItemEntity entityitem = this.spawnAtLocation((ItemLike) Sheep.ITEM_BY_DYE.get(this.level.purpurConfig.sheepShearJebRandomColor && hasCustomName() && getCustomName().getString().equals("jeb_") ? DyeColor.random(this.level.random) : this.getColor()), 1); // Purpur + this.forceDrops = false; // CraftBukkit + + if (entityitem != null) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +index 5437571ce76c62e9cae841e99127867fffb39f43..34fa428268a863e8e36b6340a482ec67f1199efb 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +@@ -49,17 +49,56 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + private static final EntityDataAccessor DATA_PUMPKIN_ID = SynchedEntityData.defineId(SnowGolem.class, EntityDataSerializers.BYTE); + private static final byte PUMPKIN_FLAG = 16; + private static final float EYE_HEIGHT = 1.7F; ++ @Nullable private java.util.UUID summoner; // Purpur + + public SnowGolem(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.snowGolemRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.snowGolemRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.snowGolemControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.snowGolemMaxHealth); ++ } ++ ++ @Nullable ++ public java.util.UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable java.util.UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.snowGolemAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { +- this.goalSelector.addGoal(1, new RangedAttackGoal(this, 1.25D, 20, 10.0F)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.goalSelector.addGoal(1, new RangedAttackGoal(this, level.purpurConfig.snowGolemAttackDistance, level.purpurConfig.snowGolemSnowBallMin, level.purpurConfig.snowGolemSnowBallMax, level.purpurConfig.snowGolemSnowBallModifier)); // Purpur + this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D, 1.0000001E-5F)); + this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(4, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Mob.class, 10, true, false, (entityliving) -> { + return entityliving instanceof Enemy; + })); +@@ -79,6 +118,7 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putBoolean("Pumpkin", this.hasPumpkin()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + } + + @Override +@@ -87,12 +127,13 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + if (nbt.contains("Pumpkin")) { + this.setPumpkin(nbt.getBoolean("Pumpkin")); + } ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + + } + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.snowGolemTakeDamageFromWater; // Purpur + } + + @Override +@@ -103,10 +144,11 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + this.hurt(this.damageSources().melting, 1.0F); // CraftBukkit - DamageSource.BURN -> CraftEventFactory.MELTING + } + +- if (!this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (!this.level.purpurConfig.snowGolemBypassMobGriefing && !this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return; + } + ++ if (getRider() != null && this.isControllable() && !level.purpurConfig.snowGolemLeaveTrailWhenRidden) return; // Purpur - don't leave snow trail when being ridden + BlockState iblockdata = Blocks.SNOW.defaultBlockState(); + + for (int i = 0; i < 4; ++i) { +@@ -154,10 +196,10 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + if (itemstack.is(Items.SHEARS) && this.readyForShearing()) { + // CraftBukkit start + if (!CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand)) { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + // CraftBukkit end +- this.shear(SoundSource.PLAYERS); ++ this.shear(SoundSource.PLAYERS, net.minecraft.world.item.enchantment.EnchantmentHelper.getMobLooting(player)); // Purpur + this.gameEvent(GameEvent.SHEAR, player); + if (!this.level.isClientSide) { + itemstack.hurtAndBreak(1, player, (entityhuman1) -> { +@@ -166,17 +208,27 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + } + + return InteractionResult.sidedSuccess(this.level.isClientSide); ++ // Purpur start ++ } else if (level.purpurConfig.snowGolemPutPumpkinBack && !hasPumpkin() && itemstack.getItem() == Blocks.CARVED_PUMPKIN.asItem()) { ++ setPumpkin(true); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return InteractionResult.SUCCESS; ++ // Purpur end + } else { +- return InteractionResult.PASS; ++ return tryRide(player, hand); // Purpur + } + } + + @Override +- public void shear(SoundSource shearedSoundCategory) { ++ public void shear(SoundSource shearedSoundCategory, int looting) { // Purpur + this.level.playSound((Player) null, (Entity) this, SoundEvents.SNOW_GOLEM_SHEAR, shearedSoundCategory, 1.0F, 1.0F); + if (!this.level.isClientSide()) { + this.setPumpkin(false); + this.forceDrops = true; // CraftBukkit ++ if (level.purpurConfig.snowGolemDropsPumpkin) // Purpur ++ for (int i = 0; i < 1 + (org.purpurmc.purpur.PurpurConfig.allowShearsLooting ? looting : 0); i++) // Purpur + this.spawnAtLocation(new ItemStack(Items.CARVED_PUMPKIN), 1.7F); + this.forceDrops = false; // CraftBukkit + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Squid.java b/src/main/java/net/minecraft/world/entity/animal/Squid.java +index 72eea6e512060fc622ca79ca87437f19a64604cc..31c89a6b8f766e1fd03608723c2d03f7f64e2e9b 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Squid.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Squid.java +@@ -46,13 +46,66 @@ public class Squid extends WaterAnimal { + + public Squid(EntityType type, Level world) { + super(type, world); +- //this.random.setSeed((long) this.getId()); // Paper - we set the random to shared, do not clobber the seed ++ if (!world.purpurConfig.entitySharedRandom) this.random.setSeed((long) this.getId()); // Paper - we set the random to shared, do not clobber the seed // Purpur + this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.squidRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.squidControllable; ++ } ++ ++ protected void rotateVectorAroundY(org.bukkit.util.Vector vector, double degrees) { ++ double rad = Math.toRadians(degrees); ++ double cos = Math.cos(rad); ++ double sine = Math.sin(rad); ++ double x = vector.getX(); ++ double z = vector.getZ(); ++ vector.setX(cos * x - sine * z); ++ vector.setZ(sine * x + cos * z); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.squidMaxHealth); ++ } ++ ++ @Override ++ public net.minecraft.world.phys.AABB getAxisForFluidCheck() { ++ // Stops squids from floating just over the water ++ return super.getAxisForFluidCheck().offsetY(level.purpurConfig.squidOffsetWaterCheck); ++ } ++ ++ public boolean canFly() { ++ return this.level.purpurConfig.squidsCanFly; ++ } ++ ++ @Override ++ public boolean isInWater() { ++ return this.wasTouchingWater || canFly(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.squidTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.squidAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new Squid.SquidRandomMovementGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Squid.SquidFleeGoal()); + } + +@@ -121,6 +174,7 @@ public class Squid extends WaterAnimal { + } + + if (this.isInWaterOrBubble()) { ++ if (canFly()) setNoGravity(!wasTouchingWater); // Purpur + if (this.tentacleMovement < 3.1415927F) { + float f = this.tentacleMovement / 3.1415927F; + +@@ -244,11 +298,43 @@ public class Squid extends WaterAnimal { + + @Override + public void tick() { ++ // Purpur start ++ Player rider = squid.getRider(); ++ if (rider != null && squid.isControllable()) { ++ if (rider.jumping) { ++ squid.onSpacebar(); ++ } ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot(); ++ float speed = (float) squid.getAttributeValue(Attributes.MOVEMENT_SPEED) * 10F; ++ if (forward < 0.0F) { ++ speed *= -0.5; ++ } ++ org.bukkit.util.Vector dir = rider.getBukkitEntity().getEyeLocation().getDirection().normalize().multiply(speed / 20.0F); ++ if (strafe != 0.0F) { ++ if (forward == 0.0F) { ++ dir.setY(0); ++ rotateVectorAroundY(dir, strafe > 0.0F ? -90 : 90); ++ } else if (forward < 0.0F) { ++ rotateVectorAroundY(dir, strafe > 0.0F ? 45 : -45); ++ } else { ++ rotateVectorAroundY(dir, strafe > 0.0F ? -45 : 45); ++ } ++ } ++ if (forward != 0.0F || strafe != 0.0F) { ++ squid.setMovementVector((float) dir.getX(), (float) dir.getY(), (float) dir.getZ()); ++ } else { ++ squid.setMovementVector(0.0F, 0.0F, 0.0F); ++ } ++ return; ++ } ++ // Purpur end ++ + int i = this.squid.getNoActionTime(); + + if (i > 100) { + this.squid.setMovementVector(0.0F, 0.0F, 0.0F); +- } else if (this.squid.getRandom().nextInt(reducedTickDelay(50)) == 0 || !this.squid.wasTouchingWater || !this.squid.hasMovementVector()) { ++ } else if (this.squid.getRandom().nextInt(reducedTickDelay(50)) == 0 || !this.squid.isInWater() || !this.squid.hasMovementVector()) { // Purpur + float f = this.squid.getRandom().nextFloat() * 6.2831855F; + float f1 = Mth.cos(f) * 0.2F; + float f2 = -0.1F + this.squid.getRandom().nextFloat() * 0.2F; +diff --git a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java +index b05b560b7570e97bc234b75f26233909fcf575b3..e4b4bf5ef228c0460fdab966d4c9b5c428f78b9a 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java ++++ b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java +@@ -42,6 +42,33 @@ public class TropicalFish extends AbstractSchoolingFish implements VariantHolder + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.tropicalFishRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.tropicalFishControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.tropicalFishMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.tropicalFishTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.tropicalFishAlwaysDropExp; ++ } ++ // Purpur end ++ + public static String getPredefinedName(int variant) { + return "entity.minecraft.tropical_fish.predefined." + variant; + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 81dab77f525ae667614f940c4ff5ec308a9579a2..52eff7a4d3a34a566bc3bc03e6643c494c757156 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -83,6 +83,43 @@ public class Turtle extends Animal { + this.setMaxUpStep(1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.turtleRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.turtleRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.turtleControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.turtleMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.turtleBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.turtleTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.turtleAlwaysDropExp; ++ } ++ // Purpur end ++ + public void setHomePos(BlockPos pos) { + this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... + } +@@ -185,6 +222,7 @@ public class Turtle extends Animal { + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new Turtle.TurtlePanicGoal(this, 1.2D)); + this.goalSelector.addGoal(1, new Turtle.TurtleBreedGoal(this, 1.0D)); + this.goalSelector.addGoal(1, new Turtle.TurtleLayEggGoal(this, 1.0D)); +@@ -342,13 +380,15 @@ public class Turtle extends Animal { + org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit + } + +- private static class TurtleMoveControl extends MoveControl { ++ private static class TurtleMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private final Turtle turtle; ++ private final org.purpurmc.purpur.controller.WaterMoveControllerWASD waterController; // Purpur + + TurtleMoveControl(Turtle turtle) { + super(turtle); + this.turtle = turtle; ++ waterController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(turtle, 0.25D); // Purpur + } + + private void updateSpeed() { +@@ -367,8 +407,18 @@ public class Turtle extends Animal { + + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ if (turtle.isInWater()) { ++ waterController.purpurTick(rider); ++ } else { ++ super.purpurTick(rider); ++ } ++ } ++ // Purpur end ++ + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + this.updateSpeed(); + if (this.operation == MoveControl.Operation.MOVE_TO && !this.turtle.getNavigation().isDone()) { + double d0 = this.wantedX - this.turtle.getX(); +@@ -384,7 +434,7 @@ public class Turtle extends Animal { + + this.turtle.setYRot(this.rotlerp(this.turtle.getYRot(), f, 90.0F)); + this.turtle.yBodyRot = this.turtle.getYRot(); +- float f1 = (float) (this.speedModifier * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED)); + + this.turtle.setSpeed(Mth.lerp(0.125F, this.turtle.getSpeed(), f1)); + this.turtle.setDeltaMovement(this.turtle.getDeltaMovement().add(0.0D, (double) this.turtle.getSpeed() * d1 * 0.1D, 0.0D)); +diff --git a/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java b/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java +index 35cfa366baf6747105faa93f1220bb9cc31a5bd5..ff3a6755d04f2280a36bd363ab1722e074e37194 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/WaterAnimal.java +@@ -82,6 +82,6 @@ public abstract class WaterAnimal extends PathfinderMob { + i = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.maximum.or(i); + j = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.minimum.or(j); + // Paper end +- return pos.getY() >= j && pos.getY() <= i && world.getFluidState(pos.below()).is(FluidTags.WATER) && world.getBlockState(pos.above()).is(Blocks.WATER); ++ return ((reason == MobSpawnType.SPAWNER && world.getMinecraftWorld().purpurConfig.spawnerFixMC238526) || (pos.getY() >= j && pos.getY() <= i)) && world.getFluidState(pos.below()).is(FluidTags.WATER) && world.getBlockState(pos.above()).is(Blocks.WATER); // Purpur + } + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Wolf.java b/src/main/java/net/minecraft/world/entity/animal/Wolf.java +index 612601b2536dc522356d4dd2c2ea1192f6435f72..e0ca657b0aea52ab6a91c256c1bfad1e5028f6e0 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Wolf.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Wolf.java +@@ -10,6 +10,7 @@ import net.minecraft.network.syncher.EntityDataAccessor; + import net.minecraft.network.syncher.EntityDataSerializers; + import net.minecraft.network.syncher.SynchedEntityData; + import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; + import net.minecraft.sounds.SoundEvent; + import net.minecraft.sounds.SoundEvents; + import net.minecraft.tags.BlockTags; +@@ -17,9 +18,12 @@ import net.minecraft.util.Mth; + import net.minecraft.util.RandomSource; + import net.minecraft.util.TimeUtil; + import net.minecraft.util.valueproviders.UniformInt; ++import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.InteractionHand; + import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.AgeableMob; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.EntityDimensions; +@@ -29,6 +33,7 @@ import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.MobSpawnType; + import net.minecraft.world.entity.NeutralMob; + import net.minecraft.world.entity.Pose; ++import net.minecraft.world.entity.SpawnGroupData; + import net.minecraft.world.entity.TamableAnimal; + import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + import net.minecraft.world.entity.ai.attributes.Attributes; +@@ -37,6 +42,7 @@ import net.minecraft.world.entity.ai.goal.BegGoal; + import net.minecraft.world.entity.ai.goal.BreedGoal; + import net.minecraft.world.entity.ai.goal.FloatGoal; + import net.minecraft.world.entity.ai.goal.FollowOwnerGoal; ++import net.minecraft.world.entity.ai.goal.Goal; + import net.minecraft.world.entity.ai.goal.LeapAtTargetGoal; + import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; + import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +@@ -64,6 +70,7 @@ import net.minecraft.world.item.ItemStack; + import net.minecraft.world.item.Items; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.LevelAccessor; ++import net.minecraft.world.level.ServerLevelAccessor; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.gameevent.GameEvent; + import net.minecraft.world.level.pathfinder.BlockPathTypes; +@@ -83,6 +90,37 @@ public class Wolf extends TamableAnimal implements NeutralMob { + + return entitytypes == EntityType.SHEEP || entitytypes == EntityType.RABBIT || entitytypes == EntityType.FOX; + }; ++ // Purpur start - rabid wolf spawn chance ++ private boolean isRabid = false; ++ private static final Predicate RABID_PREDICATE = entity -> entity instanceof ServerPlayer || entity instanceof Mob; ++ private final Goal PATHFINDER_VANILLA = new NonTameRandomTargetGoal<>(this, Animal.class, false, PREY_SELECTOR); ++ private final Goal PATHFINDER_RABID = new NonTameRandomTargetGoal<>(this, LivingEntity.class, false, RABID_PREDICATE); ++ private static final class AvoidRabidWolfGoal extends AvoidEntityGoal { ++ private final Wolf wolf; ++ ++ public AvoidRabidWolfGoal(Wolf wolf, float distance, double minSpeed, double maxSpeed) { ++ super(wolf, Wolf.class, distance, minSpeed, maxSpeed); ++ this.wolf = wolf; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && !this.wolf.isRabid() && this.toAvoid != null && this.toAvoid.isRabid(); // wolves which are not rabid run away from rabid wolves ++ } ++ ++ @Override ++ public void start() { ++ this.wolf.setTarget(null); ++ super.start(); ++ } ++ ++ @Override ++ public void tick() { ++ this.wolf.setTarget(null); ++ super.tick(); ++ } ++ } ++ // Purpur end + private static final float START_HEALTH = 8.0F; + private static final float TAME_HEALTH = 20.0F; + private float interestedAngle; +@@ -102,12 +140,93 @@ public class Wolf extends TamableAnimal implements NeutralMob { + this.setPathfindingMalus(BlockPathTypes.DANGER_POWDER_SNOW, -1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.wolfRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.wolfRidableInWater; ++ } ++ ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setInSittingPose(false); ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.wolfControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.wolfMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.wolfBreedingTicks; ++ } ++ ++ public boolean isRabid() { ++ return this.isRabid; ++ } ++ ++ public void setRabid(boolean isRabid) { ++ this.isRabid = isRabid; ++ updatePathfinders(true); ++ } ++ ++ public void updatePathfinders(boolean modifyEffects) { ++ this.targetSelector.removeGoal(PATHFINDER_VANILLA); ++ this.targetSelector.removeGoal(PATHFINDER_RABID); ++ if (this.isRabid) { ++ setTame(false); ++ setOwnerUUID(null); ++ this.targetSelector.addGoal(5, PATHFINDER_RABID); ++ if (modifyEffects) this.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 1200)); ++ } else { ++ this.targetSelector.addGoal(5, PATHFINDER_VANILLA); ++ this.stopBeingAngry(); ++ if (modifyEffects) this.removeEffect(MobEffects.CONFUSION); ++ } ++ } ++ ++ @Override ++ public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType type, @Nullable SpawnGroupData data, @Nullable CompoundTag nbt) { ++ this.isRabid = world.getLevel().purpurConfig.wolfNaturalRabid > 0.0D && random.nextDouble() <= world.getLevel().purpurConfig.wolfNaturalRabid; ++ this.updatePathfinders(false); ++ return super.finalizeSpawn(world, difficulty, type, data, nbt); ++ } ++ ++ @Override ++ public void tame(Player player) { ++ setCollarColor(level.purpurConfig.wolfDefaultCollarColor); ++ super.tame(player); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.wolfTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.wolfAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Wolf.WolfPanicGoal(1.5D)); + this.goalSelector.addGoal(2, new SitWhenOrderedToGoal(this)); + this.goalSelector.addGoal(3, new Wolf.WolfAvoidEntityGoal<>(this, Llama.class, 24.0F, 1.5D, 1.5D)); ++ this.goalSelector.addGoal(3, new AvoidRabidWolfGoal(this, 24.0F, 1.5D, 1.5D)); // Purpur + this.goalSelector.addGoal(4, new LeapAtTargetGoal(this, 0.4F)); + this.goalSelector.addGoal(5, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(6, new FollowOwnerGoal(this, 1.0D, 10.0F, 2.0F, false)); +@@ -116,11 +235,12 @@ public class Wolf extends TamableAnimal implements NeutralMob { + this.goalSelector.addGoal(9, new BegGoal(this, 8.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(10, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new OwnerHurtByTargetGoal(this)); + this.targetSelector.addGoal(2, new OwnerHurtTargetGoal(this)); + this.targetSelector.addGoal(3, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers()); + this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt)); +- this.targetSelector.addGoal(5, new NonTameRandomTargetGoal<>(this, Animal.class, false, Wolf.PREY_SELECTOR)); ++ // this.targetSelector.addGoal(5, new NonTameRandomTargetGoal<>(this, Animal.class, false, Wolf.PREY_SELECTOR)); // Purpur - moved to updatePathfinders() + this.targetSelector.addGoal(6, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR)); + this.targetSelector.addGoal(7, new NearestAttackableTargetGoal<>(this, AbstractSkeleton.class, false)); + this.targetSelector.addGoal(8, new ResetUniversalAngerTargetGoal<>(this, true)); +@@ -165,6 +285,7 @@ public class Wolf extends TamableAnimal implements NeutralMob { + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putByte("CollarColor", (byte) this.getCollarColor().getId()); ++ nbt.putBoolean("Purpur.IsRabid", this.isRabid); // Purpur + this.addPersistentAngerSaveData(nbt); + } + +@@ -174,6 +295,10 @@ public class Wolf extends TamableAnimal implements NeutralMob { + if (nbt.contains("CollarColor", 99)) { + this.setCollarColor(DyeColor.byId(nbt.getInt("CollarColor"))); + } ++ // Purpur start ++ this.isRabid = nbt.getBoolean("Purpur.IsRabid"); ++ this.updatePathfinders(false); ++ // Purpur end + + this.readPersistentAngerSaveData(this.level, nbt); + } +@@ -218,6 +343,11 @@ public class Wolf extends TamableAnimal implements NeutralMob { + public void tick() { + super.tick(); + if (this.isAlive()) { ++ // Purpur start ++ if (this.age % 300 == 0 && this.isRabid()) { ++ this.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 400)); ++ } ++ // Purpur end + this.interestedAngleO = this.interestedAngle; + if (this.isInterested()) { + this.interestedAngle += (1.0F - this.interestedAngle) * 0.4F; +@@ -412,7 +542,7 @@ public class Wolf extends TamableAnimal implements NeutralMob { + } + + // CraftBukkit - added event call and isCancelled check. +- if (this.random.nextInt(3) == 0 && !CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { ++ if ((this.level.purpurConfig.alwaysTameInCreative && player.getAbilities().instabuild) || (this.random.nextInt(3) == 0 && !CraftEventFactory.callEntityTameEvent(this, player).isCancelled())) { // Purpur + this.tame(player); + this.navigation.stop(); + this.setTarget((LivingEntity) null); +@@ -424,6 +554,20 @@ public class Wolf extends TamableAnimal implements NeutralMob { + + return InteractionResult.SUCCESS; + } ++ // Purpur start ++ else if (this.level.purpurConfig.wolfMilkCuresRabies && itemstack.getItem() == Items.MILK_BUCKET && this.isRabid()) { ++ if (!player.isCreative()) { ++ player.setItemInHand(hand, new ItemStack(Items.BUCKET)); ++ } ++ this.setRabid(false); ++ for (int i = 0; i < 10; ++i) { ++ ((ServerLevel) level).sendParticles(((ServerLevel) level).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 1.5), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + + return super.mobInteract(player, hand); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +index a41d8101c960163803179d3717889aee6182d0bb..e95540122ae6a486ce12a5f50fb4d2d073239554 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java ++++ b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +@@ -101,10 +101,23 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + private float spinningAnimationTicks; + private float spinningAnimationTicks0; + public boolean forceDancing = false; // CraftBukkit ++ private org.purpurmc.purpur.controller.FlyingMoveControllerWASD purpurController; // Purpur + + public Allay(EntityType type, Level world) { + super(type, world); +- this.moveControl = new FlyingMoveControl(this, 20, true); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this, 0.1F, 0.5F); ++ this.moveControl = new FlyingMoveControl(this, 20, true) { ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ purpurController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.setCanPickUpLoot(this.canPickUpLoot()); + EntityPositionSource entitypositionsource = new EntityPositionSource(this, this.getEyeHeight()); + +@@ -119,6 +132,28 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + } + // CraftBukkit end + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.allayRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.allayRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.allayControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(Allay.MEMORY_TYPES, Allay.SENSOR_TYPES); +@@ -226,13 +261,13 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("allayBrain"); ++ //this.level.getProfiler().push("allayBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("allayActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("allayActivityUpdate"); // Purpur + AllayAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -374,9 +409,31 @@ public class Allay extends PathfinderMob implements InventoryCarrier { + + @Override + public boolean wantsToPickUp(ItemStack stack) { +- ItemStack itemstack1 = this.getItemInHand(InteractionHand.MAIN_HAND); +- +- return !itemstack1.isEmpty() && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && this.inventory.canAddItem(stack) && this.allayConsidersItemEqual(itemstack1, stack); ++ // Purpur start ++ if (!this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ return false; ++ } ++ ItemStack itemStack = this.getItemInHand(InteractionHand.MAIN_HAND); ++ if (itemStack.isEmpty()) { ++ return false; ++ } ++ if (!allayConsidersItemEqual(itemStack, stack)) { ++ return false; ++ } ++ if (!this.inventory.canAddItem(stack)) { ++ return false; ++ } ++ for (String tag : this.level.purpurConfig.allayRespectNBT) { ++ if (stack.hasTag() && itemStack.hasTag()) { ++ Tag tag1 = stack.getTag().get(tag); ++ Tag tag2 = itemStack.getTag().get(tag); ++ if (!Objects.equals(tag1, tag2)) { ++ return false; ++ } ++ } ++ } ++ return true; ++ // Purpur end + } + + private boolean allayConsidersItemEqual(ItemStack stack, ItemStack stack2) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +index 38d21943fb2940f53c2d0ac2c3b94a6f0e46e700..18ce5389040b516a7061d2d8e104f400183fdeec 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java ++++ b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java +@@ -98,6 +98,43 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder getModelRotationValues() { + return this.modelRotationValues; +@@ -288,13 +325,13 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder optional = this.getBrain().getMemory(MemoryModuleType.PLAY_DEAD_TICKS); + +@@ -521,14 +558,22 @@ public class Axolotl extends Animal implements LerpingModel, VariantHolder brain = (Brain) this.getBrain(); // Paper - decompile fix + brain.tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("camelActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("camelActivityUpdate"); // Purpur + CamelAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -314,6 +321,23 @@ public class Camel extends AbstractHorse implements PlayerRideableJumping, Rider + return this.dashCooldown; + } + ++ // Purpur start ++ @Override ++ public float generateMaxHealth(net.minecraft.util.RandomSource random) { ++ return (float) generateMaxHealth(this.level.purpurConfig.camelMaxHealthMin, this.level.purpurConfig.camelMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(net.minecraft.util.RandomSource random) { ++ return generateJumpStrength(this.level.purpurConfig.camelJumpStrengthMin, this.level.purpurConfig.camelJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(net.minecraft.util.RandomSource random) { ++ return generateSpeed(this.level.purpurConfig.camelMovementSpeedMin, this.level.purpurConfig.camelMovementSpeedMax); ++ } ++ // Purpur end ++ + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.CAMEL_AMBIENT; +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +index be44667b8205cd3bb1cefddf8e0e743535414e14..c355aaed76663d37a5da8b2f49f9808828b4ef9b 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +@@ -82,16 +82,69 @@ public class Frog extends Animal implements VariantHolder { + public final AnimationState croakAnimationState = new AnimationState(); + public final AnimationState tongueAnimationState = new AnimationState(); + public final AnimationState swimIdleAnimationState = new AnimationState(); ++ private org.purpurmc.purpur.controller.MoveControllerWASD purpurLandController; // Purpur ++ private org.purpurmc.purpur.controller.WaterMoveControllerWASD purpurWaterController; // Purpur + + public Frog(EntityType type, Level world) { + super(type, world); + this.lookControl = new Frog.FrogLookControl(this); + this.setPathfindingMalus(BlockPathTypes.WATER, 4.0F); + this.setPathfindingMalus(BlockPathTypes.TRAPDOOR, -1.0F); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ this.purpurLandController = new org.purpurmc.purpur.controller.MoveControllerWASD(this, 0.2F); ++ this.purpurWaterController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(this, 0.5F); ++ this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true) { ++ @Override ++ public void tick() { ++ net.minecraft.world.entity.player.Player rider = mob.getRider(); ++ if (rider != null && mob.isControllable()) { ++ if (mob.isInWater()) { ++ purpurWaterController.purpurTick(rider); ++ mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, -0.005D, 0.0D)); ++ } else { ++ purpurLandController.purpurTick(rider); ++ } ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.setMaxUpStep(1.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.frogRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.frogRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.frogControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return (getRider() != null && isControllable()) ? level.purpurConfig.frogRidableJumpHeight * this.getBlockJumpFactor() : super.getJumpPower(); ++ } ++ ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.frogBreedingTicks; ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); +@@ -170,13 +223,13 @@ public class Frog extends Animal implements VariantHolder { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("frogBrain"); ++ //this.level.getProfiler().push("frogBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("frogActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("frogActivityUpdate"); // Purpur + FrogAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -376,7 +429,7 @@ public class Frog extends Animal implements VariantHolder { + return world.getBlockState(pos.below()).is(BlockTags.FROGS_SPAWNABLE_ON) && isBrightEnoughToSpawn(world, pos); + } + +- class FrogLookControl extends LookControl { ++ class FrogLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + FrogLookControl(Mob entity) { + super(entity); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +index e591b0a09f5a8475b3ec9cd28bd5d5b69809ed73..aadc6743deb195ac3368548a75be641ffd3da404 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +@@ -45,13 +45,50 @@ public class Tadpole extends AbstractFish { + protected static final ImmutableList>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.HURT_BY, SensorType.FROG_TEMPTATIONS); + protected static final ImmutableList> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.LOOK_TARGET, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.NEAREST_VISIBLE_ADULT, MemoryModuleType.TEMPTATION_COOLDOWN_TICKS, MemoryModuleType.IS_TEMPTED, MemoryModuleType.TEMPTING_PLAYER, MemoryModuleType.BREED_TARGET, MemoryModuleType.IS_PANICKING); + public boolean ageLocked; // Paper ++ private org.purpurmc.purpur.controller.WaterMoveControllerWASD purpurController; // Purpur + + public Tadpole(EntityType type, Level world) { + super(type, world); +- this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.WaterMoveControllerWASD(this, 0.5F); ++ this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true) { ++ @Override ++ public void tick() { ++ Player rider = mob.getRider(); ++ if (rider != null && mob.isControllable()) { ++ purpurController.purpurTick(rider); ++ mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, 0.002D, 0.0D)); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.lookControl = new SmoothSwimmingLookControl(this, 10); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.tadpoleRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.tadpoleRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.tadpoleControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + protected PathNavigation createNavigation(Level world) { + return new WaterBoundPathNavigation(this, world); +@@ -80,13 +117,13 @@ public class Tadpole extends AbstractFish { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("tadpoleBrain"); ++ //this.level.getProfiler().push("tadpoleBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("tadpoleActivityUpdate"); ++ //this.level.getProfiler().pop(); // Purpur ++ //this.level.getProfiler().push("tadpoleActivityUpdate"); // Purpur + TadpoleAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +index 4fdc3bd591b6df4c28901e4571aa23baf034d885..f5c0fc9f30bdf7935200b875ada0ff1011fdb034 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java +@@ -89,6 +89,38 @@ public class Goat extends Animal { + return InstrumentItem.create(Items.GOAT_HORN, (Holder) holderset.getRandomElement(randomsource).get()); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.goatRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.goatRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.goatControllable; ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.goatBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.goatTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.goatAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(Goat.MEMORY_TYPES, Goat.SENSOR_TYPES); +@@ -191,13 +223,14 @@ public class Goat extends Animal { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("goatBrain"); +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ //this.level.getProfiler().push("goatBrain"); // Purpur ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); +- this.level.getProfiler().push("goatActivityUpdate"); ++ //this.level.getProfiler().pop ++ // (); // Purpur ++ //this.level.getProfiler().push("goatActivityUpdate"); // Purpur + GoatAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +@@ -389,6 +422,7 @@ public class Goat extends Animal { + + // Paper start - Goat ram API + public void ram(net.minecraft.world.entity.LivingEntity entity) { ++ if(!new org.purpurmc.purpur.event.entity.GoatRamEntityEvent((org.bukkit.entity.Goat) getBukkitEntity(), (org.bukkit.entity.LivingEntity) entity.getBukkitLivingEntity()).callEvent()) return; // Purpur + Brain brain = this.getBrain(); + brain.setMemory(MemoryModuleType.RAM_TARGET, entity.position()); + brain.eraseMemory(MemoryModuleType.RAM_COOLDOWN_TICKS); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +index 47cd69f91bbc2e2be9ec970674adc522e21593c8..c044ed3a96f10584fd5aec836624bca1b414182d 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +@@ -144,12 +144,60 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + + protected AbstractHorse(EntityType type, Level world) { + super(type, world); ++ this.moveControl = new net.minecraft.world.entity.ai.control.MoveControl(this); // Purpur - use vanilla controller ++ this.lookControl = new net.minecraft.world.entity.ai.control.LookControl(this); // Purpur - use vanilla controller + this.setMaxUpStep(1.0F); + this.createInventory(); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return false; // vanilla handles ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.generateMaxHealth(random)); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.generateSpeed(random)); ++ this.getAttribute(Attributes.JUMP_STRENGTH).setBaseValue(this.generateJumpStrength(random)); ++ } ++ ++ protected double generateMaxHealth(double min, double max) { ++ if (min == max) return min; ++ int diff = Mth.floor(max - min); ++ double base = max - diff; ++ int first = Mth.floor((double) diff / 2); ++ int rest = diff - first; ++ return base + random.nextInt(first + 1) + random.nextInt(rest + 1); ++ } ++ ++ protected double generateJumpStrength(double min, double max) { ++ if (min == max) return min; ++ return min + (max - min) * this.random.nextDouble(); ++ } ++ ++ protected double generateSpeed(double min, double max) { ++ if (min == max) return min; ++ return min + (max - min) * this.random.nextDouble(); ++ } ++ ++ protected float generateMaxHealth(RandomSource random) { ++ return 15.0F + (float) random.nextInt(8) + (float) random.nextInt(9); ++ } ++ ++ protected double generateJumpStrength(RandomSource random) { ++ return 0.4000000059604645D + random.nextDouble() * 0.2D + random.nextDouble() * 0.2D + random.nextDouble() * 0.2D; ++ } ++ ++ protected double generateSpeed(RandomSource random) { ++ return (0.44999998807907104D + random.nextDouble() * 0.3D + random.nextDouble() * 0.3D + random.nextDouble() * 0.3D) * 0.25D; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HorseHasRider(this)); // Purpur + this.goalSelector.addGoal(1, new PanicGoal(this, 1.2D)); + this.goalSelector.addGoal(1, new RunAroundLikeCrazyGoal(this, 1.2D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D, AbstractHorse.class)); +@@ -160,6 +208,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + if (this.canPerformRearing()) { + this.goalSelector.addGoal(9, new RandomStandGoal(this)); + } ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HorseHasRider(this)); // Purpur + + this.addBehaviourGoals(); + } +@@ -336,7 +385,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + + @Override + protected int calculateFallDamage(float fallDistance, float damageMultiplier) { +- return Mth.ceil((fallDistance * 0.5F - 3.0F) * damageMultiplier); ++ return Mth.ceil((fallDistance * 0.5F - this.safeFallDistance) * damageMultiplier); + } + + protected int getInventorySize() { +@@ -1252,7 +1301,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + entityData = new AgeableMob.AgeableMobGroupData(0.2F); + } + +- this.randomizeAttributes(world.getRandom()); ++ // this.randomizeAttributes(world.getRandom()); // Purpur - replaced by initAttributes() + return super.finalizeSpawn(world, difficulty, spawnReason, (SpawnGroupData) entityData, entityNbt); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java +index e0dfee0e0ce091d5ae0ec740e939c2c50915c104..7ad29aacc73ca1cb98b76ad36b92a3edb2256629 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java +@@ -15,6 +15,43 @@ public class Donkey extends AbstractChestedHorse { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.donkeyRidableInWater; ++ } ++ ++ @Override ++ public float generateMaxHealth(net.minecraft.util.RandomSource random) { ++ return (float) generateMaxHealth(this.level.purpurConfig.donkeyMaxHealthMin, this.level.purpurConfig.donkeyMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(net.minecraft.util.RandomSource random) { ++ return generateJumpStrength(this.level.purpurConfig.donkeyJumpStrengthMin, this.level.purpurConfig.donkeyJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(net.minecraft.util.RandomSource random) { ++ return generateSpeed(this.level.purpurConfig.donkeyMovementSpeedMin, this.level.purpurConfig.donkeyMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.donkeyBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.donkeyTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.donkeyAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.DONKEY_AMBIENT; +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java +index 79a2b3c8df70a9a73ad44560a4a6129f91db8e16..fb433878731b824b4d595b7f28626f25bdfabbeb 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java +@@ -40,6 +40,43 @@ public class Horse extends AbstractHorse implements VariantHolder { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.horseRidableInWater; ++ } ++ ++ @Override ++ public float generateMaxHealth(RandomSource random) { ++ return (float) generateMaxHealth(this.level.purpurConfig.horseMaxHealthMin, this.level.purpurConfig.horseMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(RandomSource random) { ++ return generateJumpStrength(this.level.purpurConfig.horseJumpStrengthMin, this.level.purpurConfig.horseJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(RandomSource random) { ++ return generateSpeed(this.level.purpurConfig.horseMovementSpeedMin, this.level.purpurConfig.horseMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.horseBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.horseTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.horseAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void randomizeAttributes(RandomSource random) { + this.getAttribute(Attributes.MAX_HEALTH).setBaseValue((double)generateMaxHealth(random::nextInt)); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +index 7ae0e4b3aa8e861500ddc7b38aa671258b532fcd..309fd5bccadcc584354d328bd31a6f4591c2d0a0 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +@@ -73,11 +73,86 @@ public class Llama extends AbstractChestedHorse implements VariantHolder type, Level world) { + super(type, world); ++ // Purpur start ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this) { ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable() && isSaddled()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ }; ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable() && isSaddled()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ }; ++ // Purpur end ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.llamaRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.llamaRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.llamaControllable; ++ } ++ ++ @Override ++ public boolean isSaddled() { ++ return super.isSaddled() || (isTamed() && getSwag() != null); ++ } ++ ++ @Override ++ public float generateMaxHealth(RandomSource random) { ++ return (float) generateMaxHealth(this.level.purpurConfig.llamaMaxHealthMin, this.level.purpurConfig.llamaMaxHealthMax); ++ } ++ ++ @Override ++ public double generateJumpStrength(RandomSource random) { ++ return generateJumpStrength(this.level.purpurConfig.llamaJumpStrengthMin, this.level.purpurConfig.llamaJumpStrengthMax); ++ } ++ ++ @Override ++ public double generateSpeed(RandomSource random) { ++ return generateSpeed(this.level.purpurConfig.llamaMovementSpeedMin, this.level.purpurConfig.llamaMovementSpeedMax); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.llamaBreedingTicks; + } + ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.llamaTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.llamaAlwaysDropExp; ++ } ++ // Purpur end ++ + public boolean isTraderLlama() { + return false; + } +@@ -110,7 +185,7 @@ public class Llama extends AbstractChestedHorse implements VariantHolder public + super.jumpFromGround(); + double d0 = this.moveControl.getSpeedModifier(); + +@@ -443,11 +470,11 @@ public class Sniffer extends Animal { + + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("snifferBrain"); ++ //this.level.getProfiler().push("snifferBrain"); // Purpur + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().popPush("snifferActivityUpdate"); ++ //this.level.getProfiler().popPush("snifferActivityUpdate"); // Purpur + SnifferAi.updateActivity(this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java b/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java +index de84a00ce2d2b7c654b08164489624e124568346..998c72513df1dcd2b1316b320b3d5e7ca8e69fd4 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java ++++ b/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java +@@ -24,6 +24,13 @@ public class EnderDragonPart extends Entity { + this.name = name; + } + ++ // Purpur start ++ @Override ++ public net.minecraft.world.InteractionResult interact(net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand) { ++ return parentMob.isAlive() ? parentMob.tryRide(player, hand) : net.minecraft.world.InteractionResult.PASS; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + } +diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +index 64f17b4a22454b59968787089253eaba0a04c1f2..e3fe5f18c77e36479eaeb7edfd2a3eb919c342d6 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java ++++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +@@ -30,6 +30,12 @@ public class EndCrystal extends Entity { + private static final EntityDataAccessor DATA_SHOW_BOTTOM = SynchedEntityData.defineId(EndCrystal.class, EntityDataSerializers.BOOLEAN); + public int time; + public boolean generatedByDragonFight = false; // Paper - Fix invulnerable end crystals ++ // Purpur start ++ private net.minecraft.world.entity.monster.Phantom targetPhantom; ++ private int phantomBeamTicks = 0; ++ private int phantomDamageCooldown = 0; ++ private int idleCooldown = 0; ++ // Purpur end + + public EndCrystal(EntityType type, Level world) { + super(type, world); +@@ -77,9 +83,69 @@ public class EndCrystal extends Entity { + } + } + // Paper end ++ if (this.level.purpurConfig.endCrystalCramming > 0 && this.level.getEntitiesOfClass(EndCrystal.class, getBoundingBox()).size() > this.level.purpurConfig.endCrystalCramming) this.hurt(this.damageSources().cramming(), 6.0F); // Purpur + } + ++ // Purpur start ++ if (level.purpurConfig.phantomAttackedByCrystalRadius <= 0 || --idleCooldown > 0) { ++ return; // on cooldown ++ } ++ ++ if (targetPhantom == null) { ++ for (net.minecraft.world.entity.monster.Phantom phantom : level.getEntitiesOfClass(net.minecraft.world.entity.monster.Phantom.class, getBoundingBox().inflate(level.purpurConfig.phantomAttackedByCrystalRadius))) { ++ if (phantom.hasLineOfSight(this)) { ++ attackPhantom(phantom); ++ break; ++ } ++ } ++ } else { ++ setBeamTarget(new BlockPos(targetPhantom).offset(0, -2, 0)); ++ if (--phantomBeamTicks > 0 && targetPhantom.isAlive()) { ++ phantomDamageCooldown--; ++ if (targetPhantom.hasLineOfSight(this)) { ++ if (phantomDamageCooldown <= 0) { ++ phantomDamageCooldown = 20; ++ targetPhantom.hurt(targetPhantom.damageSources().indirectMagic(this, this), level.purpurConfig.phantomAttackedByCrystalDamage); ++ } ++ } else { ++ forgetPhantom(); // no longer in sight ++ } ++ } else { ++ forgetPhantom(); // attacked long enough ++ } ++ } ++ } ++ ++ private void attackPhantom(net.minecraft.world.entity.monster.Phantom phantom) { ++ phantomDamageCooldown = 0; ++ phantomBeamTicks = 60; ++ targetPhantom = phantom; ++ } ++ ++ private void forgetPhantom() { ++ targetPhantom = null; ++ setBeamTarget(null); ++ phantomBeamTicks = 0; ++ phantomDamageCooldown = 0; ++ idleCooldown = 60; ++ } ++ ++ public boolean shouldExplode() { ++ return showsBottom() ? level.purpurConfig.basedEndCrystalExplode : level.purpurConfig.baselessEndCrystalExplode; ++ } ++ ++ public float getExplosionPower() { ++ return (float) (showsBottom() ? level.purpurConfig.basedEndCrystalExplosionPower : level.purpurConfig.baselessEndCrystalExplosionPower); ++ } ++ ++ public boolean hasExplosionFire() { ++ return showsBottom() ? level.purpurConfig.basedEndCrystalExplosionFire : level.purpurConfig.baselessEndCrystalExplosionFire; ++ } ++ ++ public Level.ExplosionInteraction getExplosionEffect() { ++ return showsBottom() ? level.purpurConfig.basedEndCrystalExplosionEffect : level.purpurConfig.baselessEndCrystalExplosionEffect; + } ++ // Purpur end + + @Override + protected void addAdditionalSaveData(CompoundTag nbt) { +@@ -124,17 +190,19 @@ public class EndCrystal extends Entity { + // CraftBukkit end + this.remove(Entity.RemovalReason.KILLED); + if (!source.is(DamageTypeTags.IS_EXPLOSION)) { ++ if (shouldExplode()) {// Purpur + DamageSource damagesource1 = source.getEntity() != null ? this.damageSources().explosion(this, source.getEntity()) : null; + + // CraftBukkit start +- ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), 6.0F, false); ++ ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), getExplosionPower(), hasExplosionFire()); // Purpur + this.level.getCraftServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + this.unsetRemoved(); + return false; + } +- this.level.explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.BLOCK); ++ this.level.explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), getExplosionEffect()); // Purpur + // CraftBukkit end ++ } else this.unsetRemoved(); // Purpur + } + + this.onDestroyedBy(source); +diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +index 3f66986948d0b43a75454389b7ec8517e2d50899..b6ac41633e91f6ee2755d1f05aac4c8046a4aa8a 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java ++++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +@@ -104,9 +104,11 @@ public class EnderDragon extends Mob implements Enemy { + @Nullable + private BlockPos podium; + // Paper end ++ private boolean hadRider; // Purpur + + public EnderDragon(EntityType entitytypes, Level world) { + super(EntityType.ENDER_DRAGON, world); ++ this.explosionSource = new Explosion(world, this, null, null, Double.NaN, Double.NaN, Double.NaN, Float.NaN, true, Explosion.BlockInteraction.DESTROY); // Purpur - moved instantiation from field + this.subEntities = new EnderDragonPart[]{this.head, this.neck, this.body, this.tail1, this.tail2, this.tail3, this.wing1, this.wing2}; + this.setHealth(this.getMaxHealth()); + this.noPhysics = true; +@@ -118,8 +120,59 @@ public class EnderDragon extends Mob implements Enemy { + } + + this.phaseManager = new EnderDragonPhaseManager(this); +- this.explosionSource = new Explosion(world, this, null, null, Double.NaN, Double.NaN, Double.NaN, Float.NaN, true, Explosion.BlockInteraction.DESTROY); // CraftBukkit ++ ++ // Purpur start ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingMoveControllerWASD(this) { ++ @Override ++ public void vanillaTick() { ++ // dragon doesn't use the controller. do nothing ++ } ++ }; ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void vanillaTick() { ++ // dragon doesn't use the controller. do nothing ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot() - 180F, rider.xRotO * 0.5F); ++ } ++ }; ++ // Purpur end ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.enderDragonRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.enderDragonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.enderDragonControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.enderDragonMaxY; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.enderDragonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.enderDragonTakeDamageFromWater; + } ++ // Purpur end + + public static AttributeSupplier.Builder createAttributes() { + return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 200.0D); +@@ -182,6 +235,37 @@ public class EnderDragon extends Mob implements Enemy { + + @Override + public void aiStep() { ++ // Purpur start ++ boolean hasRider = getRider() != null && this.isControllable(); ++ if (hasRider) { ++ if (!hadRider) { ++ hadRider = true; ++ noPhysics = false; ++ this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(4.0F, 2.0F); ++ } ++ ++ // dragon doesn't use controllers, so must tick manually ++ moveControl.tick(); ++ lookControl.tick(); ++ ++ moveRelative((float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F, new Vec3(-getStrafeMot(), getVerticalMot(), -getForwardMot())); ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot); ++ move(MoverType.PLAYER, mot); ++ ++ mot = mot.multiply(0.9F, 0.9F, 0.9F); ++ setDeltaMovement(mot); ++ ++ // control wing flap speed on client ++ phaseManager.setPhase(mot.x() * mot.x() + mot.z() * mot.z() < 0.005F ? EnderDragonPhase.HOVERING : EnderDragonPhase.HOLDING_PATTERN); ++ } else if (hadRider) { ++ hadRider = false; ++ noPhysics = true; ++ this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(16.0F, 8.0F); ++ phaseManager.setPhase(EnderDragonPhase.HOLDING_PATTERN); // HoldingPattern ++ } ++ // Purpur end ++ + this.processFlappingMovement(); + if (this.level.isClientSide) { + this.setHealth(this.getHealth()); +@@ -195,6 +279,8 @@ public class EnderDragon extends Mob implements Enemy { + float f; + + if (this.isDeadOrDying()) { ++ if (hasRider) ejectPassengers(); // Purpur ++ + float f1 = (this.random.nextFloat() - 0.5F) * 8.0F; + + f = (this.random.nextFloat() - 0.5F) * 4.0F; +@@ -207,9 +293,9 @@ public class EnderDragon extends Mob implements Enemy { + + f = 0.2F / ((float) vec3d.horizontalDistance() * 10.0F + 1.0F); + f *= (float) Math.pow(2.0D, vec3d.y); +- if (this.phaseManager.getCurrentPhase().isSitting()) { ++ if (!hasRider && this.phaseManager.getCurrentPhase().isSitting()) { // Purpur + this.flapTime += 0.1F; +- } else if (this.inWall) { ++ } else if (!hasRider && this.inWall) { // Purpur + this.flapTime += f * 0.5F; + } else { + this.flapTime += f; +@@ -254,7 +340,7 @@ public class EnderDragon extends Mob implements Enemy { + } + + this.phaseManager.getCurrentPhase().doClientTick(); +- } else { ++ } else if (!hasRider) { // Purpur + DragonPhaseInstance idragoncontroller = this.phaseManager.getCurrentPhase(); + + idragoncontroller.doServerTick(); +@@ -323,7 +409,7 @@ public class EnderDragon extends Mob implements Enemy { + this.tickPart(this.body, (double) (f11 * 0.5F), 0.0D, (double) (-f12 * 0.5F)); + this.tickPart(this.wing1, (double) (f12 * 4.5F), 2.0D, (double) (f11 * 4.5F)); + this.tickPart(this.wing2, (double) (f12 * -4.5F), 2.0D, (double) (f11 * -4.5F)); +- if (!this.level.isClientSide && this.hurtTime == 0) { ++ if (!hasRider && !this.level.isClientSide && this.hurtTime == 0) { // Purpur + this.knockBack(this.level.getEntities((Entity) this, this.wing1.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); + this.knockBack(this.level.getEntities((Entity) this, this.wing2.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); + this.hurt(this.level.getEntities((Entity) this, this.head.getBoundingBox().inflate(1.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR)); +@@ -367,7 +453,7 @@ public class EnderDragon extends Mob implements Enemy { + } + + if (!this.level.isClientSide) { +- this.inWall = this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox()); ++ this.inWall = !hasRider && this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox()); // Purpur + if (this.dragonFight != null) { + this.dragonFight.updateDragon(this); + } +@@ -499,7 +585,7 @@ public class EnderDragon extends Mob implements Enemy { + BlockState iblockdata = this.level.getBlockState(blockposition); + + if (!iblockdata.isAir() && !iblockdata.is(BlockTags.DRAGON_TRANSPARENT)) { +- if (this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !iblockdata.is(BlockTags.DRAGON_IMMUNE)) { ++ if ((this.level.purpurConfig.enderDragonBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && !iblockdata.is(BlockTags.DRAGON_IMMUNE)) { // Purpur + // CraftBukkit start - Add blocks to list rather than destroying them + // flag1 = this.level.removeBlock(blockposition, false) || flag1; + flag1 = true; +@@ -634,7 +720,7 @@ public class EnderDragon extends Mob implements Enemy { + boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT); + short short0 = 500; + +- if (this.dragonFight != null && !this.dragonFight.hasPreviouslyKilledDragon()) { ++ if (this.dragonFight != null && (level.purpurConfig.enderDragonAlwaysDropsFullExp || !this.dragonFight.hasPreviouslyKilledDragon())) { + short0 = 12000; + } + +@@ -1069,6 +1155,7 @@ public class EnderDragon extends Mob implements Enemy { + + @Override + protected boolean canRide(Entity entity) { ++ if (this.level.purpurConfig.enderDragonCanRideVehicles) return this.boardingCooldown <= 0; // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +index e81e8f050bd9df34b6a64c741428503b434f03a3..4781bdd3b6c7d6b686f2fe6af530e82861385342 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java ++++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +@@ -84,16 +84,31 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + return entityliving.getMobType() != MobType.UNDEAD && entityliving.attackable(); + }; + private static final TargetingConditions TARGETING_CONDITIONS = TargetingConditions.forCombat().range(20.0D).selector(WitherBoss.LIVING_ENTITY_SELECTOR); ++ private int shootCooldown = 0; // Purpur ++ @Nullable private java.util.UUID summoner; // Purpur + // Paper start + private boolean canPortal = false; + + public void setCanTravelThroughPortals(boolean canPortal) { this.canPortal = canPortal; } + // Paper end ++ private org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD purpurController; // Purpur + + public WitherBoss(EntityType type, Level world) { + super(type, world); + this.bossEvent = (ServerBossEvent) (new ServerBossEvent(this.getDisplayName(), BossEvent.BossBarColor.PURPLE, BossEvent.BossBarOverlay.PROGRESS)).setDarkenScreen(true); +- this.moveControl = new FlyingMoveControl(this, 10, false); ++ // Purpur start ++ this.purpurController = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.1F); ++ this.moveControl = new FlyingMoveControl(this, 10, false) { ++ @Override ++ public void tick() { ++ if (mob.getRider() != null && mob.isControllable()) { ++ purpurController.purpurTick(mob.getRider()); ++ } else { ++ super.tick(); ++ } ++ } ++ }; ++ // Purpur end + this.setHealth(this.getMaxHealth()); + this.xpReward = 50; + } +@@ -108,13 +123,148 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + return navigationflying; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.witherRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.witherRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.witherControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.witherMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 5F; ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.5, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ this.entityData.set(DATA_TARGETS.get(0), 0); ++ this.entityData.set(DATA_TARGETS.get(1), 0); ++ this.entityData.set(DATA_TARGETS.get(2), 0); ++ getNavigation().stop(); ++ shootCooldown = 20; ++ } ++ ++ @Override ++ public boolean onClick(net.minecraft.world.InteractionHand hand) { ++ return shoot(getRider(), hand == net.minecraft.world.InteractionHand.MAIN_HAND ? new int[]{1} : new int[]{2}); ++ } ++ ++ public boolean shoot(@Nullable Player rider, int[] heads) { ++ if (shootCooldown > 0) { ++ return false; ++ } ++ ++ shootCooldown = 20; ++ if (rider == null) { ++ return false; ++ } ++ ++ org.bukkit.craftbukkit.entity.CraftHumanEntity player = rider.getBukkitEntity(); ++ if (!player.hasPermission("allow.special.wither")) { ++ return false; ++ } ++ ++ net.minecraft.world.phys.HitResult rayTrace = getRayTrace(120, net.minecraft.world.level.ClipContext.Fluid.NONE); ++ if (rayTrace == null) { ++ return false; ++ } ++ ++ Vec3 loc; ++ if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.BLOCK) { ++ BlockPos pos = ((net.minecraft.world.phys.BlockHitResult) rayTrace).getBlockPos(); ++ loc = new Vec3(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D); ++ } else if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.ENTITY) { ++ Entity target = ((net.minecraft.world.phys.EntityHitResult) rayTrace).getEntity(); ++ loc = new Vec3(target.getX(), target.getY() + (target.getEyeHeight() / 2), target.getZ()); ++ } else { ++ org.bukkit.block.Block block = player.getTargetBlock(null, 120); ++ loc = new Vec3(block.getX() + 0.5D, block.getY() + 0.5D, block.getZ() + 0.5D); ++ } ++ ++ for (int head : heads) { ++ shoot(head, loc.x(), loc.y(), loc.z(), rider); ++ } ++ ++ return true; // handled ++ } ++ ++ public void shoot(int head, double x, double y, double z, Player rider) { ++ level.levelEvent(null, 1024, blockPosition(), 0); ++ double headX = getHeadX(head); ++ double headY = getHeadY(head); ++ double headZ = getHeadZ(head); ++ WitherSkull skull = new WitherSkull(level, this, x - headX, y - headY, z - headZ) { ++ @Override ++ public boolean canHitEntity(Entity target) { ++ // do not hit rider ++ return target != rider && super.canHitEntity(target); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ }; ++ skull.setPosRaw(headX, headY, headZ); ++ level.addFreshEntity(skull); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.witherMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.witherTakeDamageFromWater; ++ } ++ ++ @Nullable ++ public java.util.UUID getSummoner() { ++ return summoner; ++ } ++ ++ public void setSummoner(@Nullable java.util.UUID summoner) { ++ this.summoner = summoner; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.witherAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(0, new WitherBoss.WitherDoNothingGoal()); + this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 40, 20.0F)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomFlyingGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 0, false, false, WitherBoss.LIVING_ENTITY_SELECTOR)); + } +@@ -132,6 +282,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("Invul", this.getInvulnerableTicks()); ++ if (getSummoner() != null) nbt.putUUID("Purpur.Summoner", getSummoner()); // Purpur + } + + @Override +@@ -141,6 +292,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + if (this.hasCustomName()) { + this.bossEvent.setName(this.getDisplayName()); + } ++ if (nbt.contains("Purpur.Summoner")) setSummoner(nbt.getUUID("Purpur.Summoner")); // Purpur + + } + +@@ -256,6 +408,16 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z()); ++ } ++ if (shootCooldown > 0) { ++ shootCooldown--; ++ } ++ // Purpur end ++ + int i; + + if (this.getInvulnerableTicks() > 0) { +@@ -272,7 +434,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + // CraftBukkit end + +- if (!this.isSilent()) { ++ if (!this.isSilent() && level.purpurConfig.witherPlaySpawnSound) { + // CraftBukkit start - Use relative location for far away sounds + // this.world.globalLevelEvent(1023, new BlockPosition(this), 0); + int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; +@@ -296,7 +458,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + this.setInvulnerableTicks(i); + if (this.tickCount % 10 == 0) { +- this.heal(10.0F, EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit ++ this.heal(this.getMaxHealth() / 33, EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit // Purpur + } + + } else { +@@ -356,7 +518,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + if (this.destroyBlocksTick > 0) { + --this.destroyBlocksTick; +- if (this.destroyBlocksTick == 0 && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.destroyBlocksTick == 0 && (this.level.purpurConfig.witherBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // Purpur + i = Mth.floor(this.getY()); + j = Mth.floor(this.getX()); + int i1 = Mth.floor(this.getZ()); +@@ -389,8 +551,10 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + } + +- if (this.tickCount % 20 == 0) { +- this.heal(1.0F, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit ++ // Purpur start - customizable heal rate and amount ++ if (this.tickCount % level.purpurConfig.witherHealthRegenDelay == 0) { ++ this.heal(level.purpurConfig.witherHealthRegenAmount, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit ++ // Purpur end + } + + this.bossEvent.setProgress(this.getHealth() / this.getMaxHealth()); +@@ -576,11 +740,11 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + } + + public int getAlternativeTarget(int headIndex) { +- return (Integer) this.entityData.get((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex)); ++ return getRider() != null && this.isControllable() ? 0 : this.entityData.get(WitherBoss.DATA_TARGETS.get(headIndex)); // Purpur + } + + public void setAlternativeTarget(int headIndex, int id) { +- this.entityData.set((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex), id); ++ if (getRider() == null || !this.isControllable()) this.entityData.set(WitherBoss.DATA_TARGETS.get(headIndex), id); // Purpur + } + + @Override +@@ -595,6 +759,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + + @Override + protected boolean canRide(Entity entity) { ++ if (this.level.purpurConfig.witherCanRideVehicles) return this.boardingCooldown <= 0; // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +index 3677dd991ae73428984e62e4d6fb757317987887..0545a39af0f21210ff1f5e53f6d712ae24ce43e4 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -99,10 +99,12 @@ public class ArmorStand extends LivingEntity { + private boolean noTickPoseDirty = false; + private boolean noTickEquipmentDirty = false; + // Paper end ++ public boolean canMovementTick = true; // Purpur + + public ArmorStand(EntityType type, Level world) { + super(type, world); + if (world != null) this.canTick = world.paperConfig().entities.armorStands.tick; // Paper - armour stand ticking ++ if (world != null) this.canMovementTick = world.purpurConfig.armorstandMovement; // Purpur + this.handItems = NonNullList.withSize(2, ItemStack.EMPTY); + this.armorItems = NonNullList.withSize(4, ItemStack.EMPTY); + this.headPose = ArmorStand.DEFAULT_HEAD_POSE; +@@ -112,6 +114,7 @@ public class ArmorStand extends LivingEntity { + this.leftLegPose = ArmorStand.DEFAULT_LEFT_LEG_POSE; + this.rightLegPose = ArmorStand.DEFAULT_RIGHT_LEG_POSE; + this.setMaxUpStep(0.0F); ++ this.setShowArms(world != null && world.purpurConfig.armorstandPlaceWithArms); // Purpur + } + + public ArmorStand(Level world, double x, double y, double z) { +@@ -609,7 +612,7 @@ public class ArmorStand extends LivingEntity { + private org.bukkit.event.entity.EntityDeathEvent brokenByPlayer(DamageSource damageSource) { // Paper + ItemStack itemstack = new ItemStack(Items.ARMOR_STAND); + +- if (this.hasCustomName()) { ++ if (this.level.purpurConfig.persistentDroppableEntityDisplayNames && this.hasCustomName()) { // Purpur + itemstack.setHoverName(this.getCustomName()); + } + +@@ -685,6 +688,7 @@ public class ArmorStand extends LivingEntity { + + @Override + public void tick() { ++ maxUpStep = level.purpurConfig.armorstandStepHeight; + // Paper start + if (!this.canTick) { + if (this.noTickPoseDirty) { +@@ -1006,4 +1010,18 @@ public class ArmorStand extends LivingEntity { + } + // Paper end + // Paper end ++ ++ // Purpur start ++ @Override ++ public void updateInWaterStateAndDoWaterCurrentPushing() { ++ if (this.level.purpurConfig.armorstandWaterMovement && ++ (this.level.purpurConfig.armorstandWaterFence || !(level.getBlockState(blockPosition().below()).getBlock() instanceof net.minecraft.world.level.block.FenceBlock))) ++ super.updateInWaterStateAndDoWaterCurrentPushing(); ++ } ++ ++ @Override ++ public void aiStep() { ++ if (this.canMovementTick && this.canMove) super.aiStep(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +index 30aec9dff249ae629b22318e52902361a9fa4099..25158e04c39146218b21ce5d5c963a24be68b2e2 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +@@ -272,7 +272,13 @@ public class ItemFrame extends HangingEntity { + } + + if (alwaysDrop) { +- this.spawnAtLocation(this.getFrameItemStack()); ++ // Purpur start ++ final ItemStack itemFrame = this.getFrameItemStack(); ++ if (this.level.purpurConfig.persistentDroppableEntityDisplayNames && this.hasCustomName()) { ++ itemFrame.setHoverName(this.getCustomName()); ++ } ++ this.spawnAtLocation(itemFrame); ++ // Purpur end + } + + if (!itemstack.isEmpty()) { +@@ -287,6 +293,13 @@ public class ItemFrame extends HangingEntity { + } + } + ++ // Purpur start ++ @Nullable ++ public net.minecraft.world.entity.item.ItemEntity spawnAtLocation(ItemStack stack) { ++ return this.spawnAtLocation(stack, getDirection().equals(Direction.DOWN) ? -0.6F : 0.0F); ++ } ++ // Purpur end ++ + private void removeFramedMap(ItemStack itemstack) { + // Paper start - fix MC-252817 (green map markers do not disappear) + this.getFramedMapIdFromItem(itemstack).ifPresent((i) -> { +diff --git a/src/main/java/net/minecraft/world/entity/decoration/Painting.java b/src/main/java/net/minecraft/world/entity/decoration/Painting.java +index ad0df80d1adb1d945f40e1b5f7732bb36b2ca2ff..90cab3586d3e3e290475fe8d59a69d89d3c24add 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/Painting.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/Painting.java +@@ -121,7 +121,7 @@ public class Painting extends HangingEntity implements VariantHolder holder = loadVariant(nbt).orElseGet(Painting::getDefaultVariant); ++ Holder holder = loadVariant(nbt).orElseGet(() -> (Holder.Reference) getDefaultVariant()); // Purpur - decompile error TODO: still needed? + this.setVariant(holder); + this.direction = Direction.from2DDataValue(nbt.getByte("facing")); + super.readAdditionalSaveData(nbt); +@@ -159,7 +159,13 @@ public class Painting extends HangingEntity implements VariantHolder type, Level world) { + super(type, world); +@@ -349,6 +355,15 @@ public class ItemEntity extends Entity implements TraceableEntity { + return false; + } else if (!this.getItem().getItem().canBeHurtBy(source)) { + return false; ++ // Purpur start ++ } else if ( ++ (immuneToCactus && source.is(net.minecraft.world.damagesource.DamageTypes.CACTUS)) || ++ (immuneToFire && (source.is(DamageTypeTags.IS_FIRE) && source.is(net.minecraft.world.damagesource.DamageTypes.IN_FIRE)) || ++ (immuneToLightning && source.is(net.minecraft.world.damagesource.DamageTypes.LIGHTNING_BOLT)) || ++ (immuneToExplosion && source.is(DamageTypeTags.IS_EXPLOSION))) ++ ) { ++ return false; ++ // Purpur end + } else if (this.level.isClientSide) { + return true; + } else { +@@ -547,6 +562,12 @@ public class ItemEntity extends Entity implements TraceableEntity { + this.getEntityData().set(ItemEntity.DATA_ITEM, stack); + this.getEntityData().markDirty(ItemEntity.DATA_ITEM); // CraftBukkit - SPIGOT-4591, must mark dirty + this.despawnRate = level.paperConfig().entities.spawning.altItemDespawnRate.enabled ? level.paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), level.spigotConfig.itemDespawnRate) : level.spigotConfig.itemDespawnRate; // Paper ++ // Purpur start ++ if (level.purpurConfig.itemImmuneToCactus.contains(stack.getItem())) immuneToCactus = true; ++ if (level.purpurConfig.itemImmuneToExplosion.contains(stack.getItem())) immuneToExplosion = true; ++ if (level.purpurConfig.itemImmuneToFire.contains(stack.getItem())) immuneToFire = true; ++ if (level.purpurConfig.itemImmuneToLightning.contains(stack.getItem())) immuneToLightning = true; ++ // level end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java +index f2094c52196b45adfd51d8aebcc4c46b779b0925..9c8713ef3aeb2ff203bd0328d15d80c2d78d09e9 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java +@@ -66,16 +66,19 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + protected AbstractSkeleton(EntityType type, Level world) { + super(type, world); + this.reassessWeaponGoal(); ++ this.setShouldBurnInDay(true); // Purpur + } + + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new RestrictSunGoal(this)); + this.goalSelector.addGoal(3, new FleeSunGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Wolf.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); +@@ -99,35 +102,14 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + } + + // Paper start +- private boolean shouldBurnInDay = true; ++ // private boolean shouldBurnInDay = true; // Purpur - moved to LivingEntity - keep methods for ABI compatibility + public boolean shouldBurnInDay() { return shouldBurnInDay; } + public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } + // Paper end + + @Override + public void aiStep() { +- boolean flag = shouldBurnInDay && this.isSunBurnTick(); // Paper - Configurable Burning +- +- if (flag) { +- ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); +- +- if (!itemstack.isEmpty()) { +- if (itemstack.isDamageableItem()) { +- itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); +- if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { +- this.broadcastBreakEvent(EquipmentSlot.HEAD); +- this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); +- } +- } +- +- flag = false; +- } +- +- if (flag) { +- this.setSecondsOnFire(8); +- } +- } +- ++ // Purpur start - implemented in LivingEntity + super.aiStep(); + } + +@@ -161,11 +143,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + this.reassessWeaponGoal(); + this.setCanPickUpLoot(this.level.paperConfig().entities.behavior.mobsCanAlwaysPickUpLoot.skeletons || randomsource.nextFloat() < 0.55F * difficulty.getSpecialMultiplier()); // Paper + if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) { +- LocalDate localdate = LocalDate.now(); +- int i = localdate.get(ChronoField.DAY_OF_MONTH); +- int j = localdate.get(ChronoField.MONTH_OF_YEAR); +- +- if (j == 10 && i == 31 && randomsource.nextFloat() < 0.25F) { ++ if (net.minecraft.world.entity.ambient.Bat.isHalloweenSeason(world.getMinecraftWorld()) && this.random.nextFloat() < this.level.purpurConfig.chanceHeadHalloweenOnEntity) { // Purpur + this.setItemSlot(EquipmentSlot.HEAD, new ItemStack(randomsource.nextFloat() < 0.1F ? Blocks.JACK_O_LANTERN : Blocks.CARVED_PUMPKIN)); + this.armorDropChances[EquipmentSlot.HEAD.getIndex()] = 0.0F; + } +@@ -192,7 +170,6 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + } else { + this.goalSelector.addGoal(4, this.meleeGoal); + } +- + } + } + +@@ -205,7 +182,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + double d2 = target.getZ() - this.getZ(); + double d3 = Math.sqrt(d0 * d0 + d2 * d2); + +- entityarrow.shoot(d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, (float) (14 - this.level.getDifficulty().getId() * 4)); ++ entityarrow.shoot(d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, this.level.purpurConfig.skeletonBowAccuracyMap.getOrDefault(this.level.getDifficulty().getId(), (float) (14 - this.level.getDifficulty().getId() * 4))); // Purpur + // CraftBukkit start + org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(this, this.getMainHandItem(), entityarrow.getPickupItem(), entityarrow, net.minecraft.world.InteractionHand.MAIN_HAND, 0.8F, true); // Paper + if (event.isCancelled()) { +@@ -236,7 +213,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + this.reassessWeaponGoal(); + // Paper start + if (nbt.contains("Paper.ShouldBurnInDay")) { +- this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); ++ // this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); // Purpur - implemented in LivingEntity + } + // Paper end + } +@@ -245,7 +222,7 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); +- nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); ++ // nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Purpur - implemented in LivingEntity + } + // Paper end + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Blaze.java b/src/main/java/net/minecraft/world/entity/monster/Blaze.java +index 5ae34ded698e501dc5cb97b1d7028863e95742a1..2ad81368f731a937303f17ede20f18c978b6479c 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Blaze.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Blaze.java +@@ -32,26 +32,73 @@ public class Blaze extends Monster { + + public Blaze(EntityType type, Level world) { + super(type, world); +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ this.moveControl = new org.purpurmc.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F); // Purpur ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, 0.0F); + this.xpReward = 10; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.blazeRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.blazeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.blazeControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.blazeMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.blazeMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.blazeAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Blaze.BlazeAttackGoal(this)); + this.goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 1.0D)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } + + public static AttributeSupplier.Builder createAttributes() { +- return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0D).add(Attributes.MOVEMENT_SPEED, (double)0.23F).add(Attributes.FOLLOW_RANGE, 48.0D); ++ return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0D).add(Attributes.MOVEMENT_SPEED, (double)0.23F).add(Attributes.FOLLOW_RANGE, 48.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + @Override +@@ -101,11 +148,19 @@ public class Blaze extends Monster { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.blazeTakeDamageFromWater; // Purpur + } + + @Override + protected void customServerAiStep() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ Vec3 mot = getDeltaMovement(); ++ setDeltaMovement(mot.x(), getVerticalMot() > 0 ? 0.07D : -0.07D, mot.z()); ++ return; ++ } ++ // Purpur end ++ + --this.nextHeightOffsetChangeTick; + if (this.nextHeightOffsetChangeTick <= 0) { + this.nextHeightOffsetChangeTick = 100; +diff --git a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java +index d980b906d9206560741576fa4153c57212f307a0..d23141c44a11050de6ffd12d95a0c2820c3f71e3 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java +@@ -28,6 +28,38 @@ public class CaveSpider extends Spider { + return Spider.createAttributes().add(Attributes.MAX_HEALTH, 12.0D); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.caveSpiderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.caveSpiderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.caveSpiderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.caveSpiderMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.caveSpiderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.caveSpiderAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean doHurtTarget(Entity target) { + if (super.doHurtTarget(target)) { +diff --git a/src/main/java/net/minecraft/world/entity/monster/Creeper.java b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +index 29c62525241e2e03686d1bceee740d4f54f33c54..c32eda28be3eb2c6a6933463d496ea7b6510f27e 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Creeper.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +@@ -59,21 +59,130 @@ public class Creeper extends Monster implements PowerableMob { + public int maxSwell = 30; + public int explosionRadius = 3; + private int droppedSkulls; ++ // Purpur start ++ private int spacebarCharge = 0; ++ private int prevSpacebarCharge = 0; ++ private int powerToggleDelay = 0; ++ private boolean exploding = false; ++ // Purpur end + + public Creeper(EntityType type, Level world) { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.creeperRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.creeperRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.creeperControllable; ++ } ++ ++ @Override ++ protected void customServerAiStep() { ++ if (powerToggleDelay > 0) { ++ powerToggleDelay--; ++ } ++ if (getRider() != null && this.isControllable()) { ++ if (getRider().getForwardMot() != 0 || getRider().getStrafeMot() != 0) { ++ spacebarCharge = 0; ++ setIgnited(false); ++ setSwellDir(-1); ++ } ++ if (spacebarCharge == prevSpacebarCharge) { ++ spacebarCharge = 0; ++ } ++ prevSpacebarCharge = spacebarCharge; ++ } ++ super.customServerAiStep(); ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ setIgnited(false); ++ setSwellDir(-1); ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (powerToggleDelay > 0) { ++ return true; // just toggled power, do not jump or ignite ++ } ++ spacebarCharge++; ++ if (spacebarCharge > maxSwell - 2) { ++ spacebarCharge = 0; ++ if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.powered.creeper")) { ++ powerToggleDelay = 20; ++ setPowered(!isPowered()); ++ setIgnited(false); ++ setSwellDir(-1); ++ return true; ++ } ++ } ++ if (!isIgnited()) { ++ if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0 && ++ getRider().getBukkitEntity().hasPermission("allow.special.creeper")) { ++ setIgnited(true); ++ setSwellDir(1); ++ return true; ++ } ++ } ++ return getForwardMot() == 0 && getStrafeMot() == 0; // do not jump if standing still ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.creeperMaxHealth); ++ } ++ ++ public net.minecraft.world.entity.SpawnGroupData finalizeSpawn(net.minecraft.world.level.ServerLevelAccessor world, net.minecraft.world.DifficultyInstance difficulty, net.minecraft.world.entity.MobSpawnType spawnReason, @Nullable net.minecraft.world.entity.SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { ++ double chance = world.getLevel().purpurConfig.creeperChargedChance; ++ if (chance > 0D && random.nextDouble() <= chance) { ++ setPowered(true); ++ } ++ return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.creeperTakeDamageFromWater; ++ } ++ ++ @Override ++ protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(DamageSource damagesource) { ++ if (!exploding && this.level.purpurConfig.creeperExplodeWhenKilled && damagesource.getEntity() instanceof net.minecraft.server.level.ServerPlayer) { ++ this.explodeCreeper(); ++ } ++ return super.dropAllDeathLoot(damagesource); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.creeperAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); + this.goalSelector.addGoal(2, new SwellGoal(this)); ++ this.goalSelector.addGoal(3, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Ocelot.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Cat.class, 6.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); + } +@@ -263,15 +372,17 @@ public class Creeper extends Monster implements PowerableMob { + } + + public void explodeCreeper() { ++ this.exploding = true; // Purpur + if (!this.level.isClientSide) { + float f = this.isPowered() ? 2.0F : 1.0F; ++ float multiplier = this.level.purpurConfig.creeperHealthRadius ? this.getHealth() / this.getMaxHealth() : 1; // Purpur + + // CraftBukkit start +- ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), this.explosionRadius * f, false); ++ ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), (this.explosionRadius * f) * multiplier, false); // Purpur + this.level.getCraftServer().getPluginManager().callEvent(event); + if (!event.isCancelled()) { + this.dead = true; +- this.level.explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); ++ this.level.explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), this.level.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING) && level.purpurConfig.creeperAllowGriefing ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); // Purpur + this.discard(); + this.spawnLingeringCloud(); + } else { +@@ -280,7 +391,7 @@ public class Creeper extends Monster implements PowerableMob { + } + // CraftBukkit end + } +- ++ this.exploding = false; // Purpur + } + + private void spawnLingeringCloud() { +@@ -322,6 +433,7 @@ public class Creeper extends Monster implements PowerableMob { + com.destroystokyo.paper.event.entity.CreeperIgniteEvent event = new com.destroystokyo.paper.event.entity.CreeperIgniteEvent((org.bukkit.entity.Creeper) getBukkitEntity(), ignited); + if (event.callEvent()) { + this.entityData.set(Creeper.DATA_IS_IGNITED, event.isIgnited()); ++ if (!event.isIgnited()) setSwellDir(-1); // Purpur + } + } + // Paper end +diff --git a/src/main/java/net/minecraft/world/entity/monster/Drowned.java b/src/main/java/net/minecraft/world/entity/monster/Drowned.java +index f00773e05654bdeb5463f448293aac99d2208813..a6980d85455234d4f89ff423e013f3c479bd3fe8 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Drowned.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Drowned.java +@@ -29,6 +29,7 @@ import net.minecraft.world.entity.ai.goal.MoveToBlockGoal; + import net.minecraft.world.entity.ai.goal.RandomStrollGoal; + import net.minecraft.world.entity.ai.goal.RangedAttackGoal; + import net.minecraft.world.entity.ai.goal.ZombieAttackGoal; ++import net.minecraft.world.entity.ai.goal.MoveThroughVillageGoal; + import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; + import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; + import net.minecraft.world.entity.ai.navigation.GroundPathNavigation; +@@ -68,6 +69,58 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.groundNavigation = new GroundPathNavigation(this, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.drownedRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.drownedRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.drownedControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.drownedMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.drownedSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.drownedJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.drownedJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.drownedJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.drownedTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.drownedAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void addBehaviourGoals() { + this.goalSelector.addGoal(1, new Drowned.DrownedGoToWaterGoal(this, 1.0D)); +@@ -75,10 +128,23 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.goalSelector.addGoal(2, new Drowned.DrownedAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(5, new Drowned.DrownedGoToBeachGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new Drowned.DrownedSwimUpGoal(this, 1.0D, this.level.getSeaLevel())); ++ if (level.purpurConfig.drownedBreakDoors) this.goalSelector.addGoal(6, new MoveThroughVillageGoal(this, 1.0D, true, 4, this::canBreakDoors)); + this.goalSelector.addGoal(7, new RandomStrollGoal(this, 1.0D)); + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Drowned.class})).setAlertOthers(ZombifiedPiglin.class)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::okTarget)); +- if (this.level.spigotConfig.zombieAggressiveTowardsVillager) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Paper ++ // Purpur start ++ if ( level.spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal(this, AbstractVillager.class, false) { // Spigot ++ @Override ++ public boolean canUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canUse(); ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canContinueToUse(); ++ } ++ }); ++ // Purpur end + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Axolotl.class, true, false)); + this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR)); +@@ -112,7 +178,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + + @Override + public boolean supportsBreakDoorGoal() { +- return false; ++ return level.purpurConfig.drownedBreakDoors ? true : false; + } + + @Override +@@ -259,8 +325,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.searchingForLand = targetingUnderwater; + } + +- private static class DrownedMoveControl extends MoveControl { +- ++ private static class DrownedMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + private final Drowned drowned; + + public DrownedMoveControl(Drowned drowned) { +@@ -269,7 +334,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + LivingEntity entityliving = this.drowned.getTarget(); + + if (this.drowned.wantsToSwim() && this.drowned.isInWater()) { +@@ -292,7 +357,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + + this.drowned.setYRot(this.rotlerp(this.drowned.getYRot(), f, 90.0F)); + this.drowned.yBodyRot = this.drowned.getYRot(); +- float f1 = (float) (this.speedModifier * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + float f2 = Mth.lerp(0.125F, this.drowned.getSpeed(), f1); + + this.drowned.setSpeed(f2); +@@ -302,7 +367,7 @@ public class Drowned extends Zombie implements RangedAttackMob { + this.drowned.setDeltaMovement(this.drowned.getDeltaMovement().add(0.0D, -0.008D, 0.0D)); + } + +- super.tick(); ++ super.vanillaTick(); // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java +index d02286d553c600fe7e75f48e278e380d21c5b868..916cf5137808003058a787210fc3343d75caf3d9 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java +@@ -33,6 +33,33 @@ public class ElderGuardian extends Guardian { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.elderGuardianRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.elderGuardianControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.elderGuardianMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.elderGuardianTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.elderGuardianAlwaysDropExp; ++ } ++ // Purpur end ++ + public static AttributeSupplier.Builder createAttributes() { + return Guardian.createAttributes().add(Attributes.MOVEMENT_SPEED, 0.30000001192092896D).add(Attributes.ATTACK_DAMAGE, 8.0D).add(Attributes.MAX_HEALTH, 80.0D); + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +index c2f5dabb41b172547864decc06aa632d89dff3e1..7c26e1979cdae52e2e94d24dd8c3164e815226ab 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java ++++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java +@@ -89,12 +89,40 @@ public class EnderMan extends Monster implements NeutralMob { + public EnderMan(EntityType type, Level world) { + super(type, world); + this.setMaxUpStep(1.0F); +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.endermanRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.endermanRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.endermanControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.endermanMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.endermanAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new EnderMan.EndermanFreezeWhenLookedAt(this)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F)); +@@ -102,9 +130,10 @@ public class EnderMan extends Monster implements NeutralMob { + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(10, new EnderMan.EndermanLeaveBlockGoal(this)); + this.goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt)); + this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0])); +- this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, true, false)); ++ this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, 10, true, false, (entityliving) -> entityliving.level.purpurConfig.endermanAggroEndermites && entityliving instanceof Endermite endermite && (!entityliving.level.purpurConfig.endermanAggroEndermitesOnlyIfPlayerSpawned || endermite.isPlayerSpawned()))); // Purpur + this.targetSelector.addGoal(4, new ResetUniversalAngerTargetGoal<>(this, false)); + } + +@@ -241,7 +270,7 @@ public class EnderMan extends Monster implements NeutralMob { + // Paper end + ItemStack itemstack = (ItemStack) player.getInventory().armor.get(3); + +- if (itemstack.is(Blocks.CARVED_PUMPKIN.asItem())) { ++ if (this.level.purpurConfig.endermanDisableStareAggro || itemstack.is(Blocks.CARVED_PUMPKIN.asItem()) || (this.level.purpurConfig.endermanIgnorePlayerDragonHead && itemstack.is(net.minecraft.world.item.Items.DRAGON_HEAD))) { // Purpur + return false; + } else { + Vec3 vec3d = player.getViewVector(1.0F).normalize(); +@@ -278,12 +307,12 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.endermanTakeDamageFromWater; // Purpur + } + + @Override + protected void customServerAiStep() { +- if (this.level.isDay() && this.tickCount >= this.targetChangeTime + 600) { ++ if ((getRider() == null || !this.isControllable()) && this.level.isDay() && this.tickCount >= this.targetChangeTime + 600) { // Purpur - no random teleporting + float f = this.getLightLevelDependentMagicValue(); + + if (f > 0.5F && this.level.canSeeSky(this.blockPosition()) && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.RUNAWAY)) { // Paper +@@ -404,6 +433,8 @@ public class EnderMan extends Monster implements NeutralMob { + public boolean hurt(DamageSource source, float amount) { + if (this.isInvulnerableTo(source)) { + return false; ++ } else if (getRider() != null && this.isControllable()) { return super.hurt(source, amount); // Purpur - no teleporting on damage ++ } else if (org.purpurmc.purpur.PurpurConfig.endermanShortHeight && source.is(net.minecraft.world.damagesource.DamageTypes.IN_WALL)) { return false; // Purpur - no suffocation damage if short height + } else { + boolean flag = source.getDirectEntity() instanceof ThrownPotion; + boolean flag1; +@@ -418,6 +449,7 @@ public class EnderMan extends Monster implements NeutralMob { + } else { + flag1 = flag && this.hurtWithCleanWater(source, (ThrownPotion) source.getDirectEntity(), amount); + ++ if (!flag1 && this.level.purpurConfig.endermanIgnoreProjectiles) return super.hurt(source, amount); // Purpur + if (this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.INDIRECT)) { // Paper start + for (int i = 0; i < 64; ++i) { + if (this.teleport()) { +@@ -464,7 +496,7 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean requiresCustomPersistence() { +- return super.requiresCustomPersistence() || this.getCarriedBlock() != null; ++ return super.requiresCustomPersistence() || (!this.level.purpurConfig.endermanDespawnEvenWithBlock && this.getCarriedBlock() != null); // Purpur + } + + private static class EndermanFreezeWhenLookedAt extends Goal { +@@ -511,7 +543,16 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean canUse() { +- return this.enderman.getCarriedBlock() == null ? false : (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0); ++ if (!enderman.level.purpurConfig.endermanAllowGriefing) return false; // Purpur ++ // Purpur start ++ if (this.enderman.getCarriedBlock() == null) { ++ return false; ++ } ++ if (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !this.enderman.level.purpurConfig.endermanBypassMobGriefing) { ++ return false; ++ } ++ return this.enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0; ++ // Purpur end + } + + @Override +@@ -558,7 +599,16 @@ public class EnderMan extends Monster implements NeutralMob { + + @Override + public boolean canUse() { +- return this.enderman.getCarriedBlock() != null ? false : (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(20)) == 0); ++ if (!enderman.level.purpurConfig.endermanAllowGriefing) return false; // Purpur ++ // Purpur start ++ if (this.enderman.getCarriedBlock() != null) { ++ return false; ++ } ++ if (!this.enderman.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !this.enderman.level.purpurConfig.endermanBypassMobGriefing) { ++ return false; ++ } ++ return this.enderman.getRandom().nextInt(reducedTickDelay(20)) == 0; ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Endermite.java b/src/main/java/net/minecraft/world/entity/monster/Endermite.java +index e8c3972b889fd6b348a5b0d18444d28faa813879..e6ecc47828fea09c80ed3a4c39f0d85f4d820571 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Endermite.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Endermite.java +@@ -31,20 +31,63 @@ import net.minecraft.world.level.block.state.BlockState; + public class Endermite extends Monster { + private static final int MAX_LIFE = 2400; + public int life; ++ private boolean isPlayerSpawned; // Purpur + + public Endermite(EntityType type, Level world) { + super(type, world); + this.xpReward = 3; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.endermiteRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.endermiteRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.endermiteControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.endermiteMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.endermiteTakeDamageFromWater; ++ } ++ ++ public boolean isPlayerSpawned() { ++ return this.isPlayerSpawned; ++ } ++ ++ public void setPlayerSpawned(boolean playerSpawned) { ++ this.isPlayerSpawned = playerSpawned; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.endermiteAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new ClimbOnTopOfPowderSnowGoal(this, this.level)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(3, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } +@@ -87,12 +130,14 @@ public class Endermite extends Monster { + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.life = nbt.getInt("Lifetime"); ++ this.isPlayerSpawned = nbt.getBoolean("PlayerSpawned"); // Purpur + } + + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putInt("Lifetime", this.life); ++ nbt.putBoolean("PlayerSpawned", this.isPlayerSpawned); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Evoker.java b/src/main/java/net/minecraft/world/entity/monster/Evoker.java +index 1935f1eb28724d8f03a9612a9b4ddefbbc557157..892e0c0306a21ea638649c1324b8115f24c01bd2 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Evoker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Evoker.java +@@ -48,10 +48,43 @@ public class Evoker extends SpellcasterIllager { + this.xpReward = 10; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.evokerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.evokerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.evokerControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.evokerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.evokerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.evokerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Evoker.EvokerCastingSpellGoal()); + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 0.6D, 1.0D)); + this.goalSelector.addGoal(4, new Evoker.EvokerSummonSpellGoal()); +@@ -60,6 +93,7 @@ public class Evoker extends SpellcasterIllager { + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300)); + this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300)); +@@ -317,7 +351,7 @@ public class Evoker extends SpellcasterIllager { + return false; + } else if (Evoker.this.tickCount < this.nextAttackTickCount) { + return false; +- } else if (!Evoker.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ } else if (!Evoker.this.level.purpurConfig.evokerBypassMobGriefing && !Evoker.this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + return false; + } else { + List list = Evoker.this.level.getNearbyEntities(Sheep.class, this.wololoTargeting, Evoker.this, Evoker.this.getBoundingBox().inflate(16.0D, 4.0D, 16.0D)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Ghast.java b/src/main/java/net/minecraft/world/entity/monster/Ghast.java +index bb2cb17e4e5ce142eeec18951c8948e3d6b3209c..77dcae6ecd87fade2b529386ba1360836363593a 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Ghast.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Ghast.java +@@ -44,11 +44,62 @@ public class Ghast extends FlyingMob implements Enemy { + this.moveControl = new Ghast.GhastMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.ghastRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.ghastRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.ghastControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.ghastMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.ghastMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.ghastTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.ghastAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(5, new Ghast.RandomFloatAroundGoal(this)); + this.goalSelector.addGoal(7, new Ghast.GhastLookGoal(this)); + this.goalSelector.addGoal(7, new Ghast.GhastShootFireballGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> { + return Math.abs(entityliving.getY() - this.getY()) <= 4.0D; + })); +@@ -103,7 +154,7 @@ public class Ghast extends FlyingMob implements Enemy { + } + + public static AttributeSupplier.Builder createAttributes() { +- return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D); ++ return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur + } + + @Override +@@ -160,7 +211,7 @@ public class Ghast extends FlyingMob implements Enemy { + return 2.6F; + } + +- private static class GhastMoveControl extends MoveControl { ++ private static class GhastMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + private final Ghast ghast; + private int floatDuration; +@@ -171,7 +222,7 @@ public class Ghast extends FlyingMob implements Enemy { + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO) { + if (this.floatDuration-- <= 0) { + this.floatDuration += this.ghast.getRandom().nextInt(5) + 2; +diff --git a/src/main/java/net/minecraft/world/entity/monster/Giant.java b/src/main/java/net/minecraft/world/entity/monster/Giant.java +index 41004c28edb748e12c4f868aa07b4672891197c1..4e5b9f772ba587b4e108add3758dffa665c1c3f3 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Giant.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Giant.java +@@ -1,18 +1,123 @@ + package net.minecraft.world.entity.monster; + + import net.minecraft.core.BlockPos; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.Difficulty; ++import net.minecraft.world.DifficultyInstance; + import net.minecraft.world.entity.EntityDimensions; + import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.EquipmentSlot; ++import net.minecraft.world.entity.MobSpawnType; + import net.minecraft.world.entity.Pose; ++import net.minecraft.world.entity.SpawnGroupData; + import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.ai.goal.FloatGoal; ++import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; ++import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; ++import net.minecraft.world.entity.ai.goal.MoveTowardsRestrictionGoal; ++import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; ++import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; ++import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; ++import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; ++import net.minecraft.world.entity.animal.IronGolem; ++import net.minecraft.world.entity.animal.Turtle; ++import net.minecraft.world.entity.npc.Villager; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.LevelReader; ++import net.minecraft.world.level.ServerLevelAccessor; ++ ++import javax.annotation.Nullable; + + public class Giant extends Monster { + public Giant(EntityType type, Level world) { + super(type, world); ++ this.safeFallDistance = 10.0F; // Purpur ++ } ++ ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.giantRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.giantRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.giantControllable; ++ } ++ ++ @Override ++ protected void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.giantMaxHealth); ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level.purpurConfig.giantMovementSpeed); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(this.level.purpurConfig.giantAttackDamage); ++ } ++ ++ @Override ++ protected void registerGoals() { ++ if (level.purpurConfig.giantHaveAI) { ++ this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); ++ this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 16.0F)); ++ this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 1.0D)); ++ if (level.purpurConfig.giantHaveHostileAI) { ++ this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers(ZombifiedPiglin.class)); ++ this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); ++ this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Villager.class, false)); ++ this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); ++ this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, true)); ++ } ++ } ++ } ++ ++ @Override ++ public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { ++ SpawnGroupData groupData = super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); ++ if (groupData == null) { ++ populateDefaultEquipmentSlots(this.random, difficulty); ++ populateDefaultEquipmentEnchantments(this.random, difficulty); ++ } ++ return groupData; ++ } ++ ++ @Override ++ protected void populateDefaultEquipmentSlots(net.minecraft.util.RandomSource random, DifficultyInstance difficulty) { ++ super.populateDefaultEquipmentSlots(this.random, difficulty); ++ // TODO make configurable ++ if (random.nextFloat() < (level.getDifficulty() == Difficulty.HARD ? 0.1F : 0.05F)) { ++ this.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.IRON_SWORD)); ++ } ++ } ++ ++ @Override ++ public float getJumpPower() { ++ // make giants jump as high as everything else relative to their size ++ // 1.0 makes bottom of feet about as high as their waist when they jump ++ return level.purpurConfig.giantJumpHeight; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.giantTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.giantAlwaysDropExp; + } ++ // Purpur end + + @Override + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { +@@ -25,6 +130,6 @@ public class Giant extends Monster { + + @Override + public float getWalkTargetValue(BlockPos pos, LevelReader world) { +- return world.getPathfindingCostFromLightLevels(pos); ++ return super.getWalkTargetValue(pos, world); // Purpur - fix light requirements for natural spawns + } + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Guardian.java b/src/main/java/net/minecraft/world/entity/monster/Guardian.java +index cf7e9c1db229f9e2cc05ce3046540db1d4fc4ec4..f10304b38e904528907cb36c342acf9d49935edd 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Guardian.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Guardian.java +@@ -69,15 +69,51 @@ public class Guardian extends Monster { + this.xpReward = 10; + this.setPathfindingMalus(BlockPathTypes.WATER, 0.0F); + this.moveControl = new Guardian.GuardianMoveControl(this); ++ // Purpur start ++ this.lookControl = new org.purpurmc.purpur.controller.LookControllerWASD(this) { ++ @Override ++ public void setYawPitch(float yaw, float pitch) { ++ super.setYawPitch(yaw, pitch * 0.35F); ++ } ++ }; ++ // Purpur end + this.clientSideTailAnimation = this.random.nextFloat(); + this.clientSideTailAnimationO = this.clientSideTailAnimation; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.guardianRidable; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.guardianControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.guardianMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.guardianTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.guardianAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + MoveTowardsRestrictionGoal pathfindergoalmovetowardsrestriction = new MoveTowardsRestrictionGoal(this, 1.0D); + + this.randomStrollGoal = new RandomStrollGoal(this, 1.0D, 80); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, this.guardianAttackGoal = new Guardian.GuardianAttackGoal(this)); // CraftBukkit - assign field + this.goalSelector.addGoal(5, pathfindergoalmovetowardsrestriction); + this.goalSelector.addGoal(7, this.randomStrollGoal); +@@ -86,6 +122,7 @@ public class Guardian extends Monster { + this.goalSelector.addGoal(9, new RandomLookAroundGoal(this)); + this.randomStrollGoal.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK)); + pathfindergoalmovetowardsrestriction.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 10, true, false, new Guardian.GuardianAttackSelector(this))); + } + +@@ -351,7 +388,7 @@ public class Guardian extends Monster { + @Override + public void travel(Vec3 movementInput) { + if (this.isControlledByLocalInstance() && this.isInWater()) { +- this.moveRelative(0.1F, movementInput); ++ this.moveRelative(getRider() != null && this.isControllable() ? getSpeed() : 0.1F, movementInput); // Purpur + this.move(MoverType.SELF, this.getDeltaMovement()); + this.setDeltaMovement(this.getDeltaMovement().scale(0.9D)); + if (!this.isMoving() && this.getTarget() == null) { +@@ -363,7 +400,7 @@ public class Guardian extends Monster { + + } + +- private static class GuardianMoveControl extends MoveControl { ++ private static class GuardianMoveControl extends org.purpurmc.purpur.controller.WaterMoveControllerWASD { // Purpur + + private final Guardian guardian; + +@@ -372,8 +409,17 @@ public class Guardian extends Monster { + this.guardian = guardian; + } + ++ // Purpur start + @Override +- public void tick() { ++ public void purpurTick(Player rider) { ++ super.purpurTick(rider); ++ guardian.setDeltaMovement(guardian.getDeltaMovement().add(0.0D, 0.005D, 0.0D)); ++ guardian.setMoving(guardian.getForwardMot() > 0.0F); // control tail speed ++ } ++ // Purpur end ++ ++ @Override ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO && !this.guardian.getNavigation().isDone()) { + Vec3 vec3d = new Vec3(this.wantedX - this.guardian.getX(), this.wantedY - this.guardian.getY(), this.wantedZ - this.guardian.getZ()); + double d0 = vec3d.length(); +@@ -384,7 +430,7 @@ public class Guardian extends Monster { + + this.guardian.setYRot(this.rotlerp(this.guardian.getYRot(), f, 90.0F)); + this.guardian.yBodyRot = this.guardian.getYRot(); +- float f1 = (float) (this.speedModifier * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float f1 = (float) (this.getSpeedModifier() * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur + float f2 = Mth.lerp(0.125F, this.guardian.getSpeed(), f1); + + this.guardian.setSpeed(f2); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Husk.java b/src/main/java/net/minecraft/world/entity/monster/Husk.java +index 4996347c6dde85a2dc9aa37fdf495160093fac64..a7b690c0730d0b10133f24d7ce2d9f6a0e4a7c04 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Husk.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Husk.java +@@ -20,15 +20,68 @@ public class Husk extends Zombie { + + public Husk(EntityType type, Level world) { + super(type, world); ++ this.setShouldBurnInDay(false); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.huskRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.huskRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.huskControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.huskMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.huskSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.huskJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.huskJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.huskJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.huskTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.huskAlwaysDropExp; ++ } ++ // Purpur end ++ + public static boolean checkHuskSpawnRules(EntityType type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + return checkMonsterSpawnRules(type, world, spawnReason, pos, random) && (spawnReason == MobSpawnType.SPAWNER || world.canSeeSky(pos)); + } + + @Override + public boolean isSunSensitive() { +- return false; ++ return this.shouldBurnInDay; // Purpur - moved to LivingEntity - keep methods for ABI compatibility + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java +index 10573602c9bc73713cbd6989762d3dbb6f6fcf8c..63577d941dbd21cf93bc6f88bb50922618b6b5d5 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java +@@ -59,10 +59,45 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob { + + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.illusionerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.illusionerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.illusionerControllable; ++ } ++ ++ @Override ++ protected void initAttributes() { ++ this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.level.purpurConfig.illusionerMovementSpeed); ++ this.getAttribute(Attributes.FOLLOW_RANGE).setBaseValue(this.level.purpurConfig.illusionerFollowRange); ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.illusionerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.illusionerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.illusionerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new SpellcasterIllager.SpellcasterCastingSpellGoal()); + this.goalSelector.addGoal(4, new Illusioner.IllusionerMirrorSpellGoal()); + this.goalSelector.addGoal(5, new Illusioner.IllusionerBlindnessSpellGoal()); +@@ -70,6 +105,7 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob { + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300)); + this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java +index f23d8796aec3e02a3bb23f338903f39b6ef9dcf1..11af95207f5aff52427dc216fb9929b0f536f411 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java ++++ b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java +@@ -25,6 +25,58 @@ public class MagmaCube extends Slime { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.magmaCubeRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.magmaCubeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.magmaCubeControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ return 0.42F * this.getBlockJumpFactor(); // from EntityLiving ++ } ++ ++ @Override ++ protected String getMaxHealthEquation() { ++ return level.purpurConfig.magmaCubeMaxHealth; ++ } ++ ++ @Override ++ protected String getAttackDamageEquation() { ++ return level.purpurConfig.magmaCubeAttackDamage; ++ } ++ ++ @Override ++ protected java.util.Map getMaxHealthCache() { ++ return level.purpurConfig.magmaCubeMaxHealthCache; ++ } ++ ++ @Override ++ protected java.util.Map getAttackDamageCache() { ++ return level.purpurConfig.magmaCubeAttackDamageCache; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.magmaCubeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.magmaCubeAlwaysDropExp; ++ } ++ // Purpur end ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MOVEMENT_SPEED, (double)0.2F); + } +@@ -70,10 +122,11 @@ public class MagmaCube extends Slime { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3 = this.getDeltaMovement(); + this.setDeltaMovement(vec3.x, (double)(this.getJumpPower() + (float)this.getSize() * 0.1F), vec3.z); + this.hasImpulse = true; ++ this.actualJump = false; // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/Monster.java b/src/main/java/net/minecraft/world/entity/monster/Monster.java +index 55c245d0dfa369dc6de2197ae37335fba4fae4ae..c9b40515f4c2ff1eedfc9510930c3baebc078ebd 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Monster.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Monster.java +@@ -89,6 +89,14 @@ public abstract class Monster extends PathfinderMob implements Enemy { + } + + public static boolean isDarkEnoughToSpawn(ServerLevelAccessor world, BlockPos pos, RandomSource random) { ++ // Purpur start ++ if (!world.getMinecraftWorld().purpurConfig.mobsSpawnOnPackedIce || !world.getMinecraftWorld().purpurConfig.mobsSpawnOnBlueIce) { ++ net.minecraft.world.level.block.state.BlockState spawnBlock = world.getBlockState(pos.below()); ++ if ((!world.getMinecraftWorld().purpurConfig.mobsSpawnOnPackedIce && spawnBlock.is(net.minecraft.world.level.block.Blocks.PACKED_ICE)) || (!world.getMinecraftWorld().purpurConfig.mobsSpawnOnBlueIce && spawnBlock.is(net.minecraft.world.level.block.Blocks.BLUE_ICE))) { ++ return false; ++ } ++ } ++ // Purpur end + if (world.getBrightness(LightLayer.SKY, pos) > random.nextInt(32)) { + return false; + } else { +diff --git a/src/main/java/net/minecraft/world/entity/monster/Phantom.java b/src/main/java/net/minecraft/world/entity/monster/Phantom.java +index 97fb1d2110a51498f6419841081b500b3f190370..2c00a9fdd3a6ea16ee765339857cf58521c85797 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Phantom.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Phantom.java +@@ -49,6 +49,8 @@ public class Phantom extends FlyingMob implements Enemy { + Vec3 moveTargetPoint; + public BlockPos anchorPoint; + Phantom.AttackPhase attackPhase; ++ Vec3 crystalPosition; // Purpur ++ private static final net.minecraft.world.item.crafting.Ingredient TORCH = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.item.Items.TORCH, net.minecraft.world.item.Items.SOUL_TORCH); // Purpur + + public Phantom(EntityType type, Level world) { + super(type, world); +@@ -58,8 +60,110 @@ public class Phantom extends FlyingMob implements Enemy { + this.xpReward = 5; + this.moveControl = new Phantom.PhantomMoveControl(this); + this.lookControl = new Phantom.PhantomLookControl(this); ++ this.setShouldBurnInDay(true); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.phantomRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.phantomRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.phantomControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.phantomMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable() && !onGround) { ++ float speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, speed, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder createAttributes() { ++ return Monster.createMonsterAttributes().add(Attributes.FLYING_SPEED, 3.0D); ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.special.phantom")) { ++ shoot(); ++ } ++ return false; ++ } ++ ++ public boolean shoot() { ++ org.bukkit.Location loc = ((org.bukkit.entity.LivingEntity) getBukkitEntity()).getEyeLocation(); ++ loc.setPitch(-loc.getPitch()); ++ org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(100).add(loc.toVector()); ++ ++ org.purpurmc.purpur.entity.PhantomFlames flames = new org.purpurmc.purpur.entity.PhantomFlames(level, this); ++ flames.canGrief = level.purpurConfig.phantomAllowGriefing; ++ flames.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), 1.0F, 5.0F); ++ level.addFreshEntity(flames); ++ return true; ++ } ++ ++ private double getFromCache(java.util.function.Supplier equation, java.util.function.Supplier> cache, java.util.function.Supplier defaultValue) { ++ int size = getPhantomSize(); ++ Double value = cache.get().get(size); ++ if (value == null) { ++ try { ++ value = ((Number) scriptEngine.eval("let size = " + size + "; " + equation.get())).doubleValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ value = defaultValue.get(); ++ } ++ cache.get().put(size, value); ++ } ++ return value; ++ } ++ ++ @Override ++ protected net.minecraft.world.level.storage.loot.LootContext.Builder createLootContext(boolean causedByPlayer, DamageSource source) { ++ boolean dropped = false; ++ if (lastHurtByPlayer == null && source.getEntity() instanceof net.minecraft.world.entity.boss.enderdragon.EndCrystal) { ++ if (random.nextInt(5) < 1) { ++ dropped = spawnAtLocation(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.PHANTOM_MEMBRANE)) != null; ++ } ++ } ++ if (!dropped) { ++ return super.createLootContext(causedByPlayer, source); ++ } ++ return new net.minecraft.world.level.storage.loot.LootContext.Builder((net.minecraft.server.level.ServerLevel) level); ++ } ++ ++ public boolean isCirclingCrystal() { ++ return crystalPosition != null; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.phantomTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.phantomAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean isFlapping() { + return (this.getUniqueFlapTickOffset() + this.tickCount) % Phantom.TICKS_PER_FLAP == 0; +@@ -72,9 +176,17 @@ public class Phantom extends FlyingMob implements Enemy { + + @Override + protected void registerGoals() { +- this.goalSelector.addGoal(1, new Phantom.PhantomAttackStrategyGoal()); +- this.goalSelector.addGoal(2, new Phantom.PhantomSweepAttackGoal()); +- this.goalSelector.addGoal(3, new Phantom.PhantomCircleAroundAnchorGoal()); ++ // Purpur start ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ if (level.purpurConfig.phantomOrbitCrystalRadius > 0) { ++ this.goalSelector.addGoal(1, new FindCrystalGoal(this)); ++ this.goalSelector.addGoal(2, new OrbitCrystalGoal(this)); ++ } ++ this.goalSelector.addGoal(3, new Phantom.PhantomAttackStrategyGoal()); ++ this.goalSelector.addGoal(4, new Phantom.PhantomSweepAttackGoal()); ++ this.goalSelector.addGoal(5, new Phantom.PhantomCircleAroundAnchorGoal()); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ // Purpur end + this.targetSelector.addGoal(1, new Phantom.PhantomAttackPlayerTargetGoal()); + } + +@@ -90,7 +202,10 @@ public class Phantom extends FlyingMob implements Enemy { + + private void updatePhantomSizeInfo() { + this.refreshDimensions(); +- this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue((double) (6 + this.getPhantomSize())); ++ // Purpur start ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(getFromCache(() -> this.level.purpurConfig.phantomMaxHealth, () -> this.level.purpurConfig.phantomMaxHealthCache, () -> 20.0D)); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(getFromCache(() -> this.level.purpurConfig.phantomAttackDamage, () -> this.level.purpurConfig.phantomAttackDamageCache, () -> (double) 6 + this.getPhantomSize())); ++ // Purpur end + } + + public int getPhantomSize() { +@@ -140,14 +255,12 @@ public class Phantom extends FlyingMob implements Enemy { + this.level.addParticle(ParticleTypes.MYCELIUM, this.getX() - (double) f2, this.getY() + (double) f4, this.getZ() - (double) f3, 0.0D, 0.0D, 0.0D); + } + ++ if (level.purpurConfig.phantomFlamesOnSwoop && attackPhase == AttackPhase.SWOOP) shoot(); // Purpur + } + + @Override + public void aiStep() { +- if (this.isAlive() && shouldBurnInDay && this.isSunBurnTick()) { // Paper - Configurable Burning +- this.setSecondsOnFire(8); +- } +- ++ // Purpur - moved down to shouldBurnInDay() + super.aiStep(); + } + +@@ -159,7 +272,11 @@ public class Phantom extends FlyingMob implements Enemy { + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { + this.anchorPoint = this.blockPosition().above(5); +- this.setPhantomSize(0); ++ // Purpur start ++ int min = world.getLevel().purpurConfig.phantomMinSize; ++ int max = world.getLevel().purpurConfig.phantomMaxSize; ++ this.setPhantomSize(min == max ? min : world.getRandom().nextInt(max + 1 - min) + min); ++ // Purpur end + return super.finalizeSpawn(world, difficulty, spawnReason, entityData, entityNbt); + } + +@@ -175,7 +292,7 @@ public class Phantom extends FlyingMob implements Enemy { + if (nbt.hasUUID("Paper.SpawningEntity")) { + this.spawningEntity = nbt.getUUID("Paper.SpawningEntity"); + } +- if (nbt.contains("Paper.ShouldBurnInDay")) { ++ if (false && nbt.contains("Paper.ShouldBurnInDay")) { // Purpur - implemented in LivingEntity + this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); + } + // Paper end +@@ -192,7 +309,7 @@ public class Phantom extends FlyingMob implements Enemy { + if (this.spawningEntity != null) { + nbt.putUUID("Paper.SpawningEntity", this.spawningEntity); + } +- nbt.putBoolean("Paper.ShouldBurnInDay", shouldBurnInDay); ++ // nbt.putBoolean("Paper.ShouldBurnInDay", shouldBurnInDay); // Purpur - implemented in LivingEntity + // Paper end + } + +@@ -258,8 +375,14 @@ public class Phantom extends FlyingMob implements Enemy { + } + public void setSpawningEntity(java.util.UUID entity) { this.spawningEntity = entity; } + +- private boolean shouldBurnInDay = true; +- public boolean shouldBurnInDay() { return shouldBurnInDay; } ++ // private boolean shouldBurnInDay = true; // Purpur - moved to LivingEntity - keep methods for ABI compatibility ++ // Purpur start ++ public boolean shouldBurnInDay() { ++ boolean burnFromDaylight = this.shouldBurnInDay && this.level.purpurConfig.phantomBurnInDaylight; ++ boolean burnFromLightSource = this.level.purpurConfig.phantomBurnInLight > 0 && this.level.getMaxLocalRawBrightness(blockPosition()) >= this.level.purpurConfig.phantomBurnInLight; ++ return burnFromDaylight || burnFromLightSource; ++ } ++ // Purpur End + public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; } + // Paper end + private static enum AttackPhase { +@@ -269,7 +392,125 @@ public class Phantom extends FlyingMob implements Enemy { + private AttackPhase() {} + } + +- private class PhantomMoveControl extends MoveControl { ++ // Purpur start ++ class FindCrystalGoal extends Goal { ++ private final Phantom phantom; ++ private net.minecraft.world.entity.boss.enderdragon.EndCrystal crystal; ++ private Comparator comparator; ++ ++ FindCrystalGoal(Phantom phantom) { ++ this.phantom = phantom; ++ this.comparator = Comparator.comparingDouble(phantom::distanceToSqr); ++ this.setFlags(EnumSet.of(Flag.LOOK)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ double range = maxTargetRange(); ++ List crystals = level.getEntitiesOfClass(net.minecraft.world.entity.boss.enderdragon.EndCrystal.class, phantom.getBoundingBox().inflate(range)); ++ if (crystals.isEmpty()) { ++ return false; ++ } ++ crystals.sort(comparator); ++ crystal = crystals.get(0); ++ if (phantom.distanceToSqr(crystal) > range * range) { ++ crystal = null; ++ return false; ++ } ++ return true; ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ if (crystal == null || !crystal.isAlive()) { ++ return false; ++ } ++ double range = maxTargetRange(); ++ return phantom.distanceToSqr(crystal) <= (range * range) * 2; ++ } ++ ++ @Override ++ public void start() { ++ phantom.crystalPosition = new Vec3(crystal.getX(), crystal.getY() + (phantom.random.nextInt(10) + 10), crystal.getZ()); ++ } ++ ++ @Override ++ public void stop() { ++ crystal = null; ++ phantom.crystalPosition = null; ++ super.stop(); ++ } ++ ++ private double maxTargetRange() { ++ return phantom.level.purpurConfig.phantomOrbitCrystalRadius; ++ } ++ } ++ ++ class OrbitCrystalGoal extends Goal { ++ private final Phantom phantom; ++ private float offset; ++ private float radius; ++ private float verticalChange; ++ private float direction; ++ ++ OrbitCrystalGoal(Phantom phantom) { ++ this.phantom = phantom; ++ this.setFlags(EnumSet.of(Flag.MOVE)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ return phantom.isCirclingCrystal(); ++ } ++ ++ @Override ++ public void start() { ++ this.radius = 5.0F + phantom.random.nextFloat() * 10.0F; ++ this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F; ++ this.direction = phantom.random.nextBoolean() ? 1.0F : -1.0F; ++ updateOffset(); ++ } ++ ++ @Override ++ public void tick() { ++ if (phantom.random.nextInt(350) == 0) { ++ this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F; ++ } ++ if (phantom.random.nextInt(250) == 0) { ++ ++this.radius; ++ if (this.radius > 15.0F) { ++ this.radius = 5.0F; ++ this.direction = -this.direction; ++ } ++ } ++ if (phantom.random.nextInt(450) == 0) { ++ this.offset = phantom.random.nextFloat() * 2.0F * 3.1415927F; ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.distanceToSqr(phantom.getX(), phantom.getY(), phantom.getZ()) < 4.0D) { ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.y < phantom.getY() && !phantom.level.isEmptyBlock(new BlockPos(phantom).below(1))) { ++ this.verticalChange = Math.max(1.0F, this.verticalChange); ++ updateOffset(); ++ } ++ if (phantom.moveTargetPoint.y > phantom.getY() && !phantom.level.isEmptyBlock(new BlockPos(phantom).above(1))) { ++ this.verticalChange = Math.min(-1.0F, this.verticalChange); ++ updateOffset(); ++ } ++ } ++ ++ private void updateOffset() { ++ this.offset += this.direction * 15.0F * 0.017453292F; ++ phantom.moveTargetPoint = phantom.crystalPosition.add( ++ this.radius * Mth.cos(this.offset), ++ -4.0F + this.verticalChange, ++ this.radius * Mth.sin(this.offset)); ++ } ++ } ++ // Purpur end ++ ++ private class PhantomMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + private float speed = 0.1F; + +@@ -277,8 +518,19 @@ public class Phantom extends FlyingMob implements Enemy { + super(entity); + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ if (!Phantom.this.onGround) { ++ // phantom is always in motion when flying ++ // TODO - FIX THIS ++ // rider.setForward(1.0F); ++ } ++ super.purpurTick(rider); ++ } ++ // Purpur end ++ + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (Phantom.this.horizontalCollision) { + Phantom.this.setYRot(Phantom.this.getYRot() + 180.0F); + this.speed = 0.1F; +@@ -324,14 +576,20 @@ public class Phantom extends FlyingMob implements Enemy { + } + } + +- private class PhantomLookControl extends LookControl { ++ private class PhantomLookControl extends org.purpurmc.purpur.controller.LookControllerWASD { // Purpur + + public PhantomLookControl(Mob entity) { + super(entity); + } + ++ // Purpur start ++ public void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot(), -rider.xRotO * 0.75F); ++ } ++ // Purpur end ++ + @Override +- public void tick() {} ++ public void vanillaTick() {} // Purpur + } + + private class PhantomBodyRotationControl extends BodyRotationControl { +@@ -418,6 +676,12 @@ public class Phantom extends FlyingMob implements Enemy { + return false; + } else if (!entityliving.isAlive()) { + return false; ++ // Purpur start ++ } else if (level.purpurConfig.phantomBurnInLight > 0 && level.getLightEmission(new BlockPos(Phantom.this)) >= level.purpurConfig.phantomBurnInLight) { ++ return false; ++ } else if (level.purpurConfig.phantomIgnorePlayersWithTorch && (TORCH.test(entityliving.getItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND)) || TORCH.test(entityliving.getItemInHand(net.minecraft.world.InteractionHand.OFF_HAND)))) { ++ return false; ++ // Purpur end + } else { + if (entityliving instanceof Player) { + Player entityhuman = (Player) entityliving; +@@ -563,6 +827,7 @@ public class Phantom extends FlyingMob implements Enemy { + this.nextScanTick = reducedTickDelay(60); + List list = Phantom.this.level.getNearbyPlayers(this.attackTargeting, Phantom.this, Phantom.this.getBoundingBox().inflate(16.0D, 64.0D, 16.0D)); + ++ if (level.purpurConfig.phantomIgnorePlayersWithTorch) list.removeIf(human -> TORCH.test(human.getItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND)) || TORCH.test(human.getItemInHand(net.minecraft.world.InteractionHand.OFF_HAND)));// Purpur + if (!list.isEmpty()) { + list.sort(Comparator.comparing((Entity e) -> { return e.getY(); }).reversed()); // CraftBukkit - decompile error + Iterator iterator = list.iterator(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Pillager.java b/src/main/java/net/minecraft/world/entity/monster/Pillager.java +index cec545c3baa6599d47b9cf1a4b97de8771062a22..06a96eb0ef40462932892c611f308eb31411d099 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Pillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Pillager.java +@@ -62,15 +62,49 @@ public class Pillager extends AbstractIllager implements CrossbowAttackMob, Inve + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.pillagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.pillagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.pillagerControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.pillagerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.pillagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.pillagerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new Raider.HoldGroundAttackGoal(this, 10.0F)); + this.goalSelector.addGoal(3, new RangedCrossbowAttackGoal<>(this, 1.0D, 8.0F)); + this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D)); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 15.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 15.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, Raider.class)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Ravager.java b/src/main/java/net/minecraft/world/entity/monster/Ravager.java +index 9258d0f7c5c27b6d3d8f99db947169d6800d8ea9..0099595a5daa9c0ca9e3fd35933038c1c8ecf009 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Ravager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Ravager.java +@@ -65,14 +65,54 @@ public class Ravager extends Raider { + this.setPathfindingMalus(BlockPathTypes.LEAVES, 0.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.ravagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.ravagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.ravagerControllable; ++ } ++ ++ @Override ++ public void onMount(Player rider) { ++ super.onMount(rider); ++ getNavigation().stop(); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.ravagerMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.ravagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.ravagerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Ravager.RavagerMeleeAttackGoal()); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.4D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(2, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true, (entityliving) -> { +@@ -150,7 +190,7 @@ public class Ravager extends Raider { + @Override + public void aiStep() { + super.aiStep(); +- if (this.isAlive()) { ++ if (this.isAlive() && (getRider() == null || !this.isControllable())) { // Purpur + if (this.isImmobile()) { + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.0D); + } else { +@@ -160,7 +200,7 @@ public class Ravager extends Raider { + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(Mth.lerp(0.1D, d1, d0)); + } + +- if (this.horizontalCollision && this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (this.horizontalCollision && (this.level.purpurConfig.ravagerBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // Purpur + boolean flag = false; + AABB axisalignedbb = this.getBoundingBox().inflate(0.2D); + Iterator iterator = BlockPos.betweenClosed(Mth.floor(axisalignedbb.minX), Mth.floor(axisalignedbb.minY), Mth.floor(axisalignedbb.minZ), Mth.floor(axisalignedbb.maxX), Mth.floor(axisalignedbb.maxY), Mth.floor(axisalignedbb.maxZ)).iterator(); +@@ -170,7 +210,7 @@ public class Ravager extends Raider { + BlockState iblockdata = this.level.getBlockState(blockposition); + Block block = iblockdata.getBlock(); + +- if (block instanceof LeavesBlock && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.getFluidState().createLegacyBlock()).isCancelled()) { // CraftBukkit // Paper ++ if (this.level.purpurConfig.ravagerGriefableBlocks.contains(block) && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.getFluidState().createLegacyBlock()).isCancelled()) { // CraftBukkit // Paper + flag = this.level.destroyBlock(blockposition, true, this) || flag; + } + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +index 8cb910da17d75a9d9c7dbeb3c9e24b6de657a2f7..8b03a027bff592b2257e065f328da6d86e11db98 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +@@ -22,6 +22,8 @@ import net.minecraft.tags.DamageTypeTags; + import net.minecraft.util.Mth; + import net.minecraft.world.Difficulty; + import net.minecraft.world.DifficultyInstance; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; + import net.minecraft.world.damagesource.DamageSource; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.EntityDimensions; +@@ -50,6 +52,8 @@ import net.minecraft.world.entity.projectile.AbstractArrow; + import net.minecraft.world.entity.projectile.ShulkerBullet; + import net.minecraft.world.entity.vehicle.Boat; + import net.minecraft.world.item.DyeColor; ++import net.minecraft.world.item.DyeItem; ++import net.minecraft.world.item.ItemStack; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.ServerLevelAccessor; + import net.minecraft.world.level.block.Blocks; +@@ -98,12 +102,59 @@ public class Shulker extends AbstractGolem implements VariantHolder= f) { ++ if ((!this.level.purpurConfig.shulkerSpawnFromBulletRequireOpenLid || !this.isClosed()) && this.teleportSomewhere()) { ++ // Purpur start ++ float chance = this.level.purpurConfig.shulkerSpawnFromBulletBaseChance; ++ if (!this.level.purpurConfig.shulkerSpawnFromBulletNearbyEquation.isBlank()) { ++ int nearby = this.level.getEntities((EntityTypeTest) EntityType.SHULKER, axisalignedbb.inflate(this.level.purpurConfig.shulkerSpawnFromBulletNearbyRange), Entity::isAlive).size(); ++ try { ++ chance -= ((Number) scriptEngine.eval("let nearby = " + nearby + "; " + this.level.purpurConfig.shulkerSpawnFromBulletNearbyEquation)).floatValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ chance -= (nearby - 1) / 5.0F; ++ } ++ } ++ if (this.level.random.nextFloat() <= chance) { ++ // Purpur end + Shulker entityshulker = (Shulker) EntityType.SHULKER.create(this.level); + + if (entityshulker != null) { +@@ -601,7 +661,7 @@ public class Shulker extends AbstractGolem implements VariantHolder getVariant() { +- return Optional.ofNullable(this.getColor()); ++ return Optional.ofNullable(this.level.purpurConfig.shulkerSpawnFromBulletRandomColor ? DyeColor.random(this.level.random) : this.getColor()); // Purpur + } + + @Nullable +@@ -611,7 +671,7 @@ public class Shulker extends AbstractGolem implements VariantHolder(this, Player.class, true)); + } +@@ -184,7 +218,7 @@ public class Silverfish extends Monster { + continue; + } + // CraftBukkit end +- if (world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ if (world.purpurConfig.silverfishBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur + world.destroyBlock(blockposition1, true, this.silverfish); + } else { + world.setBlock(blockposition1, ((InfestedBlock) block).hostStateByInfested(world.getBlockState(blockposition1)), 3); +@@ -222,7 +256,7 @@ public class Silverfish extends Monster { + } else { + RandomSource randomsource = this.mob.getRandom(); + +- if (this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && randomsource.nextInt(reducedTickDelay(10)) == 0) { ++ if ((this.mob.level.purpurConfig.silverfishBypassMobGriefing || this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && randomsource.nextInt(reducedTickDelay(10)) == 0) { // Purpur + this.selectedDirection = Direction.getRandom(randomsource); + BlockPos blockposition = BlockPos.containing(this.mob.getX(), this.mob.getY() + 0.5D, this.mob.getZ()).relative(this.selectedDirection); + BlockState iblockdata = this.mob.level.getBlockState(blockposition); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java +index badde621357a567965f0ef203e402e21bed09059..64a5e000adbfa5de2abc32ea9182847dbf83293d 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java +@@ -14,6 +14,16 @@ import net.minecraft.world.item.Items; + import net.minecraft.world.level.ItemLike; + import net.minecraft.world.level.Level; + ++// Purpur start ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.block.Blocks; ++import org.bukkit.craftbukkit.event.CraftEventFactory; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.core.particles.ParticleTypes; ++// Purpur end ++ + public class Skeleton extends AbstractSkeleton { + + private static final int TOTAL_CONVERSION_TIME = 300; +@@ -26,6 +36,38 @@ public class Skeleton extends AbstractSkeleton { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.skeletonRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.skeletonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.skeletonControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.skeletonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.skeletonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.skeletonAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -142,4 +184,67 @@ public class Skeleton extends AbstractSkeleton { + } + + } ++ ++ // Purpur start ++ private int witherRosesFed = 0; ++ ++ @Override ++ public InteractionResult mobInteract(Player player, InteractionHand hand) { ++ ItemStack stack = player.getItemInHand(hand); ++ ++ if (level.purpurConfig.skeletonFeedWitherRoses > 0 && this.getType() != EntityType.WITHER_SKELETON && stack.getItem() == Blocks.WITHER_ROSE.asItem()) { ++ return this.feedWitherRose(player, stack); ++ } ++ ++ return super.mobInteract(player, hand); ++ } ++ ++ private InteractionResult feedWitherRose(Player player, ItemStack stack) { ++ if (++witherRosesFed < level.purpurConfig.skeletonFeedWitherRoses) { ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ return InteractionResult.CONSUME; ++ } ++ ++ WitherSkeleton skeleton = EntityType.WITHER_SKELETON.create(level); ++ if (skeleton == null) { ++ return InteractionResult.PASS; ++ } ++ ++ skeleton.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); ++ skeleton.setHealth(this.getHealth()); ++ skeleton.setAggressive(this.isAggressive()); ++ skeleton.copyPosition(this); ++ skeleton.setYBodyRot(this.yBodyRot); ++ skeleton.setYHeadRot(this.getYHeadRot()); ++ skeleton.yRotO = this.yRotO; ++ skeleton.xRotO = this.xRotO; ++ ++ if (this.hasCustomName()) { ++ skeleton.setCustomName(this.getCustomName()); ++ } ++ ++ if (CraftEventFactory.callEntityTransformEvent(this, skeleton, org.bukkit.event.entity.EntityTransformEvent.TransformReason.INFECTION).isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ ++ if (!new com.destroystokyo.paper.event.entity.EntityTransformedEvent(this.getBukkitEntity(), skeleton.getBukkitEntity(), com.destroystokyo.paper.event.entity.EntityTransformedEvent.TransformedReason.INFECTED).callEvent()) { ++ return InteractionResult.PASS; ++ } ++ ++ this.level.addFreshEntity(skeleton); ++ this.remove(RemovalReason.DISCARDED); ++ if (!player.getAbilities().instabuild) { ++ stack.shrink(1); ++ } ++ ++ for (int i = 0; i < 15; ++i) { ++ ((ServerLevel) level).sendParticles(((ServerLevel) level).players(), null, ParticleTypes.HAPPY_VILLAGER, ++ getX() + random.nextFloat(), getY() + (random.nextFloat() * 2), getZ() + random.nextFloat(), 1, ++ random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, random.nextGaussian() * 0.05D, 0, true); ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Slime.java b/src/main/java/net/minecraft/world/entity/monster/Slime.java +index c41c1c2712920c6b7d822cd0f37a5d8d725e4054..89978fcb14362af2527693f3e6ec57e169080c9f 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Slime.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Slime.java +@@ -64,6 +64,7 @@ public class Slime extends Mob implements Enemy { + public float squish; + public float oSquish; + private boolean wasOnGround; ++ protected boolean actualJump; // Purpur + + public Slime(EntityType type, Level world) { + super(type, world); +@@ -71,12 +72,89 @@ public class Slime extends Mob implements Enemy { + this.moveControl = new Slime.SlimeMoveControl(this); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.slimeRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.slimeRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.slimeControllable; ++ } ++ ++ @Override ++ public float getJumpPower() { ++ float height = super.getJumpPower(); ++ return getRider() != null && this.isControllable() && actualJump ? height * 1.5F : height; ++ } ++ ++ @Override ++ public boolean onSpacebar() { ++ if (onGround && getRider() != null && this.isControllable()) { ++ actualJump = true; ++ if (getRider().getForwardMot() == 0 || getRider().getStrafeMot() == 0) { ++ jumpFromGround(); // jump() here if not moving ++ } ++ } ++ return true; // do not jump() in wasd controller, let vanilla controller handle ++ } ++ ++ protected String getMaxHealthEquation() { ++ return level.purpurConfig.slimeMaxHealth; ++ } ++ ++ protected String getAttackDamageEquation() { ++ return level.purpurConfig.slimeAttackDamage; ++ } ++ ++ protected java.util.Map getMaxHealthCache() { ++ return level.purpurConfig.slimeMaxHealthCache; ++ } ++ ++ protected java.util.Map getAttackDamageCache() { ++ return level.purpurConfig.slimeAttackDamageCache; ++ } ++ ++ protected double getFromCache(java.util.function.Supplier equation, java.util.function.Supplier> cache, java.util.function.Supplier defaultValue) { ++ int size = getSize(); ++ Double value = cache.get().get(size); ++ if (value == null) { ++ try { ++ value = ((Number) scriptEngine.eval("let size = " + size + "; " + equation.get())).doubleValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ value = defaultValue.get(); ++ } ++ cache.get().put(size, value); ++ } ++ return value; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.slimeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.slimeAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Slime.SlimeFloatGoal(this)); + this.goalSelector.addGoal(2, new Slime.SlimeAttackGoal(this)); + this.goalSelector.addGoal(3, new Slime.SlimeRandomDirectionGoal(this)); + this.goalSelector.addGoal(5, new Slime.SlimeKeepOnJumpingGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> { + return Math.abs(entityliving.getY() - this.getY()) <= 4.0D; + })); +@@ -96,9 +174,9 @@ public class Slime extends Mob implements Enemy { + this.entityData.set(Slime.ID_SIZE, j); + this.reapplyPosition(); + this.refreshDimensions(); +- this.getAttribute(Attributes.MAX_HEALTH).setBaseValue((double) (j * j)); ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(getFromCache(this::getMaxHealthEquation, this::getMaxHealthCache, () -> (double) size * size)); // Purpur + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue((double) (0.2F + 0.1F * (float) j)); +- this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue((double) j); ++ this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(getFromCache(this::getAttackDamageEquation, this::getAttackDamageCache, () -> (double) j)); // Purpur + if (heal) { + this.setHealth(this.getMaxHealth()); + } +@@ -368,11 +446,12 @@ public class Slime extends Mob implements Enemy { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(vec3d.x, (double) this.getJumpPower(), vec3d.z); + this.hasImpulse = true; ++ this.actualJump = false; // Purpur + } + + @Nullable +@@ -406,7 +485,7 @@ public class Slime extends Mob implements Enemy { + return super.getDimensions(pose).scale(0.255F * (float) this.getSize()); + } + +- private static class SlimeMoveControl extends MoveControl { ++ private static class SlimeMoveControl extends org.purpurmc.purpur.controller.MoveControllerWASD { // Purpur + + private float yRot; + private int jumpDelay; +@@ -425,21 +504,33 @@ public class Slime extends Mob implements Enemy { + } + + public void setWantedMovement(double speed) { +- this.speedModifier = speed; ++ this.setSpeedModifier(speed); // Purpur + this.operation = MoveControl.Operation.MOVE_TO; + } + + @Override + public void tick() { ++ // Purpur start ++ if (slime.getRider() != null && slime.isControllable()) { ++ purpurTick(slime.getRider()); ++ if (slime.getForwardMot() != 0 || slime.getStrafeMot() != 0) { ++ if (jumpDelay > 10) { ++ jumpDelay = 6; ++ } ++ } else { ++ jumpDelay = 20; ++ } ++ } else { ++ // Purpur end + this.mob.setYRot(this.rotlerp(this.mob.getYRot(), this.yRot, 90.0F)); + this.mob.yHeadRot = this.mob.getYRot(); + this.mob.yBodyRot = this.mob.getYRot(); +- if (this.operation != MoveControl.Operation.MOVE_TO) { ++ } if ((slime.getRider() == null || !slime.isControllable()) && this.operation != MoveControl.Operation.MOVE_TO) { // Purpur + this.mob.setZza(0.0F); + } else { + this.operation = MoveControl.Operation.WAIT; + if (this.mob.isOnGround()) { +- this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); ++ this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (slime.getRider() != null && slime.isControllable() && (slime.getRider().getForwardMot() != 0 || slime.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur + if (this.jumpDelay-- <= 0) { + this.jumpDelay = this.slime.getJumpDelay(); + if (this.isAggressive) { +@@ -456,7 +547,7 @@ public class Slime extends Mob implements Enemy { + this.mob.setSpeed(0.0F); + } + } else { +- this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); ++ this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (slime.getRider() != null && slime.isControllable() && (slime.getRider().getForwardMot() != 0 || slime.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java +index 0c36bb47bd7040f1544817810e1c87157cdaff96..8e071a0922164970e033029c12058db9e8da261a 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java +@@ -51,14 +51,48 @@ public class Spider extends Monster { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.spiderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.spiderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.spiderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.spiderMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.spiderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.spiderAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(3, new LeapAtTargetGoal(this, 0.4F)); + this.goalSelector.addGoal(4, new Spider.SpiderAttackGoal(this)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0])); + this.targetSelector.addGoal(2, new Spider.SpiderTargetGoal<>(this, Player.class)); + this.targetSelector.addGoal(3, new Spider.SpiderTargetGoal<>(this, IronGolem.class)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Stray.java b/src/main/java/net/minecraft/world/entity/monster/Stray.java +index 118b636a44e4b062e812e433f603b039276337da..677b304c177a1e2bdaddf3044b44a06395ee6b6e 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Stray.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Stray.java +@@ -21,6 +21,38 @@ public class Stray extends AbstractSkeleton { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.strayRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.strayRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.strayControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.strayMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.strayTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.strayAlwaysDropExp; ++ } ++ // Purpur end ++ + public static boolean checkStraySpawnRules(EntityType type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + BlockPos blockPos = pos; + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Strider.java b/src/main/java/net/minecraft/world/entity/monster/Strider.java +index 66755efc54059dfb8625f028bf0548d188a57aa2..0b5d3837536d526c25ba1e12be142bb476d03519 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Strider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Strider.java +@@ -94,12 +94,44 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + super(type, world); + this.steering = new ItemBasedSteering(this.entityData, Strider.DATA_BOOST_TIME, Strider.DATA_SADDLE_ID); + this.blocksBuilding = true; +- this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); ++ if (isSensitiveToWater()) this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F); // Purpur + this.setPathfindingMalus(BlockPathTypes.LAVA, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, 0.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.striderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.striderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.striderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.striderMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.striderBreedingTicks; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.striderAlwaysDropExp; ++ } ++ // Purpur end ++ + public static boolean checkStriderSpawnRules(EntityType type, LevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { + BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable(); + +@@ -161,6 +193,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + @Override + protected void registerGoals() { + this.panicGoal = new PanicGoal(this, 1.65D); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, this.panicGoal); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.temptGoal = new TemptGoal(this, 1.4D, Strider.TEMPT_ITEMS, false); +@@ -416,7 +449,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + + @Override + public boolean isSensitiveToWater() { +- return true; ++ return this.level.purpurConfig.striderTakeDamageFromWater; // Purpur + } + + @Override +@@ -458,6 +491,19 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + public InteractionResult mobInteract(Player player, InteractionHand hand) { + boolean flag = this.isFood(player.getItemInHand(hand)); + ++ // Purpur start ++ if (level.purpurConfig.striderGiveSaddleBack && player.isSecondaryUseActive() && !flag && isSaddled() && !isVehicle()) { ++ this.steering.setSaddle(false); ++ if (!player.getAbilities().instabuild) { ++ ItemStack saddle = new ItemStack(Items.SADDLE); ++ if (!player.getInventory().add(saddle)) { ++ player.drop(saddle, false); ++ } ++ } ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end ++ + if (!flag && this.isSaddled() && !this.isVehicle() && !player.isSecondaryUseActive()) { + if (!this.level.isClientSide) { + player.startRiding(this); +@@ -470,7 +516,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable { + if (!enuminteractionresult.consumesAction()) { + ItemStack itemstack = player.getItemInHand(hand); + +- return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, this, hand) : InteractionResult.PASS; ++ return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, this, hand) : tryRide(player, hand); // Purpur + } else { + if (flag && !this.isSilent()) { + this.level.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.STRIDER_EAT, this.getSoundSource(), 1.0F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Vex.java b/src/main/java/net/minecraft/world/entity/monster/Vex.java +index bb5c2f90bef5e3c57ffde996853e122d108b2789..4c4c4d52e2be963024106783b4d28713f125e2e6 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Vex.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Vex.java +@@ -63,6 +63,65 @@ public class Vex extends Monster implements TraceableEntity { + this.xpReward = 3; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.vexRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.vexRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.vexControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level.purpurConfig.vexMaxY; ++ } ++ ++ @Override ++ public void travel(Vec3 vec3) { ++ super.travel(vec3); ++ if (getRider() != null && this.isControllable()) { ++ float speed; ++ if (onGround) { ++ speed = (float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F; ++ } else { ++ speed = (float) getAttributeValue(Attributes.FLYING_SPEED); ++ } ++ setSpeed(speed); ++ Vec3 mot = getDeltaMovement(); ++ move(MoverType.SELF, mot.multiply(speed, 1.0, speed)); ++ setDeltaMovement(mot.scale(0.9D)); ++ } ++ } ++ ++ @Override ++ public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { ++ return false; // no fall damage please ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.vexMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.vexTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.vexAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { + return dimensions.height - 0.28125F; +@@ -81,7 +140,7 @@ public class Vex extends Monster implements TraceableEntity { + + @Override + public void tick() { +- this.noPhysics = true; ++ this.noPhysics = getRider() == null || !this.isControllable(); // Purpur + super.tick(); + this.noPhysics = false; + this.setNoGravity(true); +@@ -96,17 +155,19 @@ public class Vex extends Monster implements TraceableEntity { + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(4, new Vex.VexChargeAttackGoal()); + this.goalSelector.addGoal(8, new Vex.VexRandomMoveGoal()); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers()); + this.targetSelector.addGoal(2, new Vex.VexCopyOwnerTargetGoal(this)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } + + public static AttributeSupplier.Builder createAttributes() { +- return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D); ++ return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur; + } + + @Override +@@ -235,14 +296,14 @@ public class Vex extends Monster implements TraceableEntity { + return 0.4D; + } + +- private class VexMoveControl extends MoveControl { ++ private class VexMoveControl extends org.purpurmc.purpur.controller.FlyingMoveControllerWASD { // Purpur + + public VexMoveControl(Vex entityvex) { + super(entityvex); + } + + @Override +- public void tick() { ++ public void vanillaTick() { // Purpur + if (this.operation == MoveControl.Operation.MOVE_TO) { + Vec3 vec3d = new Vec3(this.wantedX - Vex.this.getX(), this.wantedY - Vex.this.getY(), this.wantedZ - Vex.this.getZ()); + double d0 = vec3d.length(); +@@ -251,7 +312,7 @@ public class Vex extends Monster implements TraceableEntity { + this.operation = MoveControl.Operation.WAIT; + Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().scale(0.5D)); + } else { +- Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.speedModifier * 0.05D / d0))); ++ Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.getSpeedModifier() * 0.05D / d0))); // Purpur + if (Vex.this.getTarget() == null) { + Vec3 vec3d1 = Vex.this.getDeltaMovement(); + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java +index a9e75a16a7dc0ff5d4f0faa92ebc444559a39325..1a333dce35a13b88cb0afdea192585e0bae38442 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java +@@ -58,14 +58,48 @@ public class Vindicator extends AbstractIllager { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.vindicatorRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.vindicatorRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.vindicatorControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.vindicatorMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.vindicatorTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.vindicatorAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); + this.goalSelector.addGoal(0, new FloatGoal(this)); ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(1, new Vindicator.VindicatorBreakDoorGoal(this)); + this.goalSelector.addGoal(2, new AbstractIllager.RaiderOpenDoorGoal(this)); + this.goalSelector.addGoal(3, new Raider.HoldGroundAttackGoal(this, 10.0F)); + this.goalSelector.addGoal(4, new Vindicator.VindicatorMeleeAttackGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, Raider.class)).setAlertOthers()); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true)); +@@ -130,6 +164,12 @@ public class Vindicator extends AbstractIllager { + RandomSource randomSource = world.getRandom(); + this.populateDefaultEquipmentSlots(randomSource, difficulty); + this.populateDefaultEquipmentEnchantments(randomSource, difficulty); ++ // Purpur start ++ Level level = world.getMinecraftWorld(); ++ if (level.purpurConfig.vindicatorJohnnySpawnChance > 0D && random.nextDouble() <= level.purpurConfig.vindicatorJohnnySpawnChance) { ++ setCustomName(Component.translatable("Johnny")); ++ } ++ // Purpur end + return spawnGroupData; + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Witch.java b/src/main/java/net/minecraft/world/entity/monster/Witch.java +index 096546d7a97f031060bda7545aa620d522766719..dcf8cdb8343706b55df206fed70fe3a8373e27a6 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Witch.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Witch.java +@@ -57,6 +57,38 @@ public class Witch extends Raider implements RangedAttackMob { + super(type, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.witchRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.witchRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.witchControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.witchMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.witchTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.witchAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + super.registerGoals(); +@@ -65,10 +97,12 @@ public class Witch extends Raider implements RangedAttackMob { + }); + this.attackPlayersGoal = new NearestAttackableWitchTargetGoal<>(this, Player.class, 10, true, false, (Predicate) null); + this.goalSelector.addGoal(1, new FloatGoal(this)); ++ this.goalSelector.addGoal(1, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 60, 10.0F)); + this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(3, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[]{Raider.class})); + this.targetSelector.addGoal(2, this.healRaidersGoal); + this.targetSelector.addGoal(3, this.attackPlayersGoal); +diff --git a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java +index 6449213d717271bcc516e393a78dfe1e5c762d68..9016fb6da9c86ca9906f6beb2f6927cede50c804 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java ++++ b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java +@@ -35,6 +35,38 @@ public class WitherSkeleton extends AbstractSkeleton { + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.witherSkeletonRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.witherSkeletonRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.witherSkeletonControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.witherSkeletonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.witherSkeletonTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.witherSkeletonAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractPiglin.class, true)); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java +index 5956a7759964f5e4939f062e93714fba64f53141..052eac2c63ecaa052c9fe6ea3d3d1000da8a33fa 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java +@@ -67,6 +67,38 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zoglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.zoglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zoglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zoglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zoglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zoglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); +@@ -182,7 +214,7 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + + @Override + public Brain getBrain() { +- return super.getBrain(); ++ return (Brain) super.getBrain(); // Purpur - decompile error + } + + protected void updateActivity() { +@@ -198,9 +230,10 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("zoglinBrain"); ++ //this.level.getProfiler().push("zoglinBrain"); // Purpur ++ if (getRider() == null || !this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + this.updateActivity(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +index 9976205537cfe228735687f1e9c52c74ac025690..36d37e544e342e1bc584374580dbb5c883523204 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +@@ -95,22 +95,69 @@ public class Zombie extends Monster { + private int inWaterTime; + public int conversionTime; + private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field +- private boolean shouldBurnInDay = true; // Paper ++ // private boolean shouldBurnInDay = true; // Paper // Purpur - implemented in LivingEntity + + public Zombie(EntityType type, Level world) { + super(type, world); + this.breakDoorGoal = new BreakDoorGoal(this, com.google.common.base.Predicates.in(world.paperConfig().entities.behavior.doorBreakingDifficulty.getOrDefault(type, world.paperConfig().entities.behavior.doorBreakingDifficulty.get(EntityType.ZOMBIE)))); // Paper ++ this.setShouldBurnInDay(true); // Purpur + } + + public Zombie(Level world) { + this(EntityType.ZOMBIE, world); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zombieRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.zombieRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zombieControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zombieMaxHealth); ++ } ++ ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.zombieJockeyOnlyBaby; ++ } ++ ++ public double jockeyChance() { ++ return level.purpurConfig.zombieJockeyChance; ++ } ++ ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.zombieJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zombieTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zombieAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + if (level.paperConfig().entities.behavior.zombiesTargetTurtleEggs) this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0D, 3)); // Paper + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.addBehaviourGoals(); + } + +@@ -120,7 +167,19 @@ public class Zombie extends Monster { + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers(ZombifiedPiglin.class)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); +- if ( level.spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Spigot ++ // Purpur start ++ if ( level.spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal(this, AbstractVillager.class, false) { // Spigot ++ @Override ++ public boolean canUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canUse(); ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return (level.purpurConfig.zombieAggressiveTowardsVillagerWhenLagging || !level.getServer().server.isLagging()) && super.canContinueToUse(); ++ } ++ }); ++ // Purpur end + this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true)); + this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR)); + } +@@ -242,30 +301,7 @@ public class Zombie extends Monster { + + @Override + public void aiStep() { +- if (this.isAlive()) { +- boolean flag = this.isSunSensitive() && this.isSunBurnTick(); +- +- if (flag) { +- ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); +- +- if (!itemstack.isEmpty()) { +- if (itemstack.isDamageableItem()) { +- itemstack.setDamageValue(itemstack.getDamageValue() + this.random.nextInt(2)); +- if (itemstack.getDamageValue() >= itemstack.getMaxDamage()) { +- this.broadcastBreakEvent(EquipmentSlot.HEAD); +- this.setItemSlot(EquipmentSlot.HEAD, ItemStack.EMPTY); +- } +- } +- +- flag = false; +- } +- +- if (flag) { +- this.setSecondsOnFire(8); +- } +- } +- } +- ++ // Purpur - implemented in LivingEntity + super.aiStep(); + } + +@@ -303,6 +339,7 @@ public class Zombie extends Monster { + + } + ++ public boolean shouldBurnInDay() { return isSunSensitive(); } // Purpur - for ABI compatibility + public boolean isSunSensitive() { + return this.shouldBurnInDay; // Paper - use api value instead + } +@@ -432,7 +469,7 @@ public class Zombie extends Monster { + nbt.putBoolean("CanBreakDoors", this.canBreakDoors()); + nbt.putInt("InWaterTime", this.isInWater() ? this.inWaterTime : -1); + nbt.putInt("DrownedConversionTime", this.isUnderWaterConverting() ? this.conversionTime : -1); +- nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper ++ // nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper // Purpur - implemented in LivingEntity + } + + @Override +@@ -446,7 +483,7 @@ public class Zombie extends Monster { + } + // Paper start + if (nbt.contains("Paper.ShouldBurnInDay")) { +- this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); ++ // this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay"); // Purpur - implemented in LivingEntity + } + // Paper end + +@@ -527,19 +564,20 @@ public class Zombie extends Monster { + if (object instanceof Zombie.ZombieGroupData) { + Zombie.ZombieGroupData entityzombie_groupdatazombie = (Zombie.ZombieGroupData) object; + +- if (entityzombie_groupdatazombie.isBaby) { +- this.setBaby(true); ++ // Purpur start ++ if (!jockeyOnlyBaby() || entityzombie_groupdatazombie.isBaby) { ++ this.setBaby(entityzombie_groupdatazombie.isBaby); + if (entityzombie_groupdatazombie.canSpawnJockey) { +- if ((double) randomsource.nextFloat() < 0.05D) { +- List list = world.getEntitiesOfClass(Chicken.class, this.getBoundingBox().inflate(5.0D, 3.0D, 5.0D), EntitySelector.ENTITY_NOT_BEING_RIDDEN); ++ if ((double) randomsource.nextFloat() < jockeyChance()) { ++ List list = jockeyTryExistingChickens() ? world.getEntitiesOfClass(Chicken.class, this.getBoundingBox().inflate(5.0D, 3.0D, 5.0D), EntitySelector.ENTITY_NOT_BEING_RIDDEN) : java.util.Collections.emptyList(); ++ // Purpur end + + if (!list.isEmpty()) { + Chicken entitychicken = (Chicken) list.get(0); + + entitychicken.setChickenJockey(true); + this.startRiding(entitychicken); +- } +- } else if ((double) randomsource.nextFloat() < 0.05D) { ++ } else { // Purpur + Chicken entitychicken1 = (Chicken) EntityType.CHICKEN.create(this.level); + + if (entitychicken1 != null) { +@@ -549,6 +587,7 @@ public class Zombie extends Monster { + this.startRiding(entitychicken1); + world.addFreshEntity(entitychicken1, CreatureSpawnEvent.SpawnReason.MOUNT); // CraftBukkit + } ++ } // Purpur + } + } + } +@@ -559,11 +598,7 @@ public class Zombie extends Monster { + } + + if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) { +- LocalDate localdate = LocalDate.now(); +- int i = localdate.get(ChronoField.DAY_OF_MONTH); +- int j = localdate.get(ChronoField.MONTH_OF_YEAR); +- +- if (j == 10 && i == 31 && randomsource.nextFloat() < 0.25F) { ++ if (net.minecraft.world.entity.ambient.Bat.isHalloweenSeason(world.getMinecraftWorld()) && this.random.nextFloat() < this.level.purpurConfig.chanceHeadHalloweenOnEntity) { // Purpur + this.setItemSlot(EquipmentSlot.HEAD, new ItemStack(randomsource.nextFloat() < 0.1F ? Blocks.JACK_O_LANTERN : Blocks.CARVED_PUMPKIN)); + this.armorDropChances[EquipmentSlot.HEAD.getIndex()] = 0.0F; + } +@@ -595,7 +630,7 @@ public class Zombie extends Monster { + } + + protected void randomizeReinforcementsChance() { +- this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * 0.10000000149011612D); ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.zombieSpawnReinforcements); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +index 71a36cf9b976443cca9ab63cd0eb23253f638562..0c6e8e05014125427513e96c32510125ec34ece9 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +@@ -79,6 +79,58 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + }); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zombieVillagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.zombieVillagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zombieVillagerControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zombieVillagerMaxHealth); ++ } ++ ++ @Override ++ protected void randomizeReinforcementsChance() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.zombieVillagerSpawnReinforcements); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.zombieVillagerJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.zombieVillagerJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.zombieVillagerJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zombieVillagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zombieVillagerAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -165,13 +217,13 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + ItemStack itemstack = player.getItemInHand(hand); + + if (itemstack.is(Items.GOLDEN_APPLE)) { +- if (this.hasEffect(MobEffects.WEAKNESS)) { ++ if (this.hasEffect(MobEffects.WEAKNESS) && level.purpurConfig.zombieVillagerCureEnabled) { // Purpur + if (!player.getAbilities().instabuild) { + itemstack.shrink(1); + } + + if (!this.level.isClientSide) { +- this.startConverting(player.getUUID(), this.random.nextInt(2401) + 3600); ++ this.startConverting(player.getUUID(), this.random.nextInt(level.purpurConfig.zombieVillagerCuringTimeMax - level.purpurConfig.zombieVillagerCuringTimeMin + 1) + level.purpurConfig.zombieVillagerCuringTimeMin); // Purpur + } + + return InteractionResult.SUCCESS; +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java +index b75945807b425609394c343da56c316a769f0a29..d3bcfa017967db0a20c18c65e27c2a0471d2214e 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java +@@ -63,6 +63,53 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.zombifiedPiglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.zombifiedPiglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.zombifiedPiglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.zombifiedPiglinMaxHealth); ++ } ++ ++ @Override ++ public boolean jockeyOnlyBaby() { ++ return level.purpurConfig.zombifiedPiglinJockeyOnlyBaby; ++ } ++ ++ @Override ++ public double jockeyChance() { ++ return level.purpurConfig.zombifiedPiglinJockeyChance; ++ } ++ ++ @Override ++ public boolean jockeyTryExistingChickens() { ++ return level.purpurConfig.zombifiedPiglinJockeyTryExistingChickens; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.zombifiedPiglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.zombifiedPiglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public void setPersistentAngerTarget(@Nullable UUID angryAt) { + this.persistentAngerTarget = angryAt; +@@ -115,7 +162,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.maybeAlertOthers(); + } + +- if (this.isAngry()) { ++ if (this.isAngry() && this.level.purpurConfig.zombifiedPiglinCountAsPlayerKillWhenAngry) { // Purpur + this.lastHurtByPlayerTime = this.tickCount; + } + +@@ -170,7 +217,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + this.ticksUntilNextAlert = ZombifiedPiglin.ALERT_INTERVAL.sample(this.random); + } + +- if (entityliving instanceof Player) { ++ if (entityliving instanceof Player && this.level.purpurConfig.zombifiedPiglinCountAsPlayerKillWhenAngry) { // Purpur + this.setLastHurtByPlayer((Player) entityliving); + } + +@@ -250,7 +297,7 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob { + + @Override + protected void randomizeReinforcementsChance() { +- this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(0.0D); ++ this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE).setBaseValue(this.random.nextDouble() * this.level.purpurConfig.zombifiedPiglinSpawnReinforcements); // Purpur + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +index daa2224b021c966751eb39f269ffbfe6e7f3d426..63ffb8c04f1408d028af086ba907551a05b96e72 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java +@@ -67,6 +67,43 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.hoglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.hoglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.hoglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.hoglinMaxHealth); ++ } ++ ++ @Override ++ public int getPurpurBreedTime() { ++ return this.level.purpurConfig.hoglinBreedingTicks; ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.hoglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.hoglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public boolean canBeLeashed(Player player) { + return !this.isLeashed(); +@@ -129,10 +166,10 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("hoglinBrain"); +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ //this.level.getProfiler().push("hoglinBrain"); // Purpur ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + HoglinAi.updateActivity(this); + if (this.isConverting()) { + ++this.timeInOverworld; +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +index b401fb4f276ca81b4bb18426ee56abed8a9f7a7b..c8db72dbdaedddb712f20cf0e3a9c731312307a0 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java +@@ -97,6 +97,38 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + this.xpReward = 5; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.piglinRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.piglinRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.piglinControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.piglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.piglinTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.piglinAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); +@@ -311,10 +343,10 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("piglinBrain"); +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ //this.level.getProfiler().push("piglinBrain"); // Purpur ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel) this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + PiglinAi.updateActivity(this); + super.customServerAiStep(); + } +@@ -410,7 +442,7 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + + @Override + public boolean wantsToPickUp(ItemStack stack) { +- return this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && this.canPickUpLoot() && PiglinAi.wantsToPickup(this, stack); ++ return (this.level.purpurConfig.piglinBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && this.canPickUpLoot() && PiglinAi.wantsToPickup(this, stack); + } + + protected boolean canReplaceCurrentItem(ItemStack stack) { +diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java +index ac75c54e897565e340b66823caeed92ba1d1641a..df4d1745c4957e564bab11d68a37178fdb398543 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java ++++ b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java +@@ -41,6 +41,38 @@ public class PiglinBrute extends AbstractPiglin { + this.xpReward = 20; + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.piglinBruteRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.piglinBruteRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.piglinBruteControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.piglinBruteMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.piglinBruteTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.piglinBruteAlwaysDropExp; ++ } ++ // Purpur end ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 50.0D).add(Attributes.MOVEMENT_SPEED, (double)0.35F).add(Attributes.ATTACK_DAMAGE, 7.0D); + } +@@ -70,7 +102,7 @@ public class PiglinBrute extends AbstractPiglin { + + @Override + public Brain getBrain() { +- return super.getBrain(); ++ return (Brain) super.getBrain(); // Purpur - decompile error + } + + @Override +@@ -85,9 +117,10 @@ public class PiglinBrute extends AbstractPiglin { + + @Override + protected void customServerAiStep() { +- this.level.getProfiler().push("piglinBruteBrain"); ++ //this.level.getProfiler().push("piglinBruteBrain"); // Purpur ++ if (getRider() == null || this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + PiglinBruteAi.updateActivity(this); + PiglinBruteAi.maybePlayActivitySound(this); + super.customServerAiStep(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +index 907d77dd74066c723238155b42028a811365b1f8..69e5b4b6c8d5725bc2fb7cd819219e4ff9df45bd 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java ++++ b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +@@ -120,8 +120,32 @@ public class Warden extends Monster implements VibrationListener.VibrationListen + this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F); + this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, 0.0F); + this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, 0.0F); ++ this.moveControl = new org.purpurmc.purpur.controller.MoveControllerWASD(this, 0.5F); // Purpur + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.wardenRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.wardenRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.wardenControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur ++ } ++ // Purpur end ++ + @Override + public Packet getAddEntityPacket() { + return new ClientboundAddEntityPacket(this, this.hasPose(Pose.EMERGING) ? 1 : 0); +@@ -275,10 +299,10 @@ public class Warden extends Monster implements VibrationListener.VibrationListen + protected void customServerAiStep() { + ServerLevel worldserver = (ServerLevel) this.level; + +- worldserver.getProfiler().push("wardenBrain"); ++ //worldserver.getProfiler().push("wardenBrain"); // Purpur + if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish + this.getBrain().tick(worldserver, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + super.customServerAiStep(); + if ((this.tickCount + this.getId()) % 120 == 0) { + Warden.applyDarknessAround(worldserver, this.position(), this, 20); +@@ -405,19 +429,16 @@ public class Warden extends Monster implements VibrationListener.VibrationListen + + @Contract("null->false") + public boolean canTargetEntity(@Nullable Entity entity) { +- boolean flag; +- ++ if (getRider() != null && isControllable()) return false; // Purpur + if (entity instanceof LivingEntity) { + LivingEntity entityliving = (LivingEntity) entity; + + if (this.level == entity.level && EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(entity) && !this.isAlliedTo(entity) && entityliving.getType() != EntityType.ARMOR_STAND && entityliving.getType() != EntityType.WARDEN && !entityliving.isInvulnerable() && !entityliving.isDeadOrDying() && this.level.getWorldBorder().isWithinBounds(entityliving.getBoundingBox())) { +- flag = true; +- return flag; ++ return true; // Purpur - wtf + } + } + +- flag = false; +- return flag; ++ return false; // Purpur - wtf + } + + public static void applyDarknessAround(ServerLevel world, Vec3 pos, @Nullable Entity entity, int range) { +diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +index ca96b893e22de3ae7c11d5cded51edf70bdcb6f2..d4ab27de59e9c533789f062e74ceb453483e2e39 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -43,6 +43,7 @@ import org.bukkit.event.entity.VillagerAcquireTradeEvent; + // CraftBukkit end + + public abstract class AbstractVillager extends AgeableMob implements InventoryCarrier, Npc, Merchant { ++ static final net.minecraft.world.item.crafting.Ingredient TEMPT_ITEMS = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.level.block.Blocks.EMERALD_BLOCK.asItem()); // Purpur + + // CraftBukkit start + private CraftMerchant craftMerchant; +diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +index 5f407535298a31a34cfe114dd863fd6a9b977707..29c7e33fe961020e5a0007287fe9b6631689f1b8 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +@@ -30,7 +30,7 @@ public class CatSpawner implements CustomSpawner { + if (this.nextTick > 0) { + return 0; + } else { +- this.nextTick = 1200; ++ this.nextTick = world.purpurConfig.catSpawnDelay; // Purpur + Player player = world.getRandomPlayer(); + if (player == null) { + return 0; +@@ -63,11 +63,15 @@ public class CatSpawner implements CustomSpawner { + } + + private int spawnInVillage(ServerLevel world, BlockPos pos) { +- int i = 48; ++ // Purpur start ++ int range = world.purpurConfig.catSpawnVillageScanRange; ++ if (range <= 0) return 0; ++ + if (world.getPoiManager().getCountInRange((entry) -> { + return entry.is(PoiTypes.HOME); +- }, pos, 48, PoiManager.Occupancy.IS_OCCUPIED) > 4L) { +- List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(48.0D, 8.0D, 48.0D)); ++ }, pos, range, PoiManager.Occupancy.IS_OCCUPIED) > 4L) { ++ List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(range, 8.0D, range)); ++ // Purpur end + if (list.size() < 5) { + return this.spawnCat(pos, world); + } +@@ -77,8 +81,11 @@ public class CatSpawner implements CustomSpawner { + } + + private int spawnInHut(ServerLevel world, BlockPos pos) { +- int i = 16; +- List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(16.0D, 8.0D, 16.0D)); ++ // Purpur start ++ int range = world.purpurConfig.catSpawnSwampHutScanRange; ++ if (range <= 0) return 0; ++ List list = world.getEntitiesOfClass(Cat.class, (new AABB(pos)).inflate(range, 8.0D, range)); ++ // Purpur end + return list.size() < 1 ? this.spawnCat(pos, world) : 0; + } + +diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java +index 5402a084ef5fe0b3cfea897a90cffade1eff5b66..73cdb6b1793b264e1ec8ff51c4f8274366a7d4d7 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -139,6 +139,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + }, MemoryModuleType.MEETING_POINT, (entityvillager, holder) -> { + return holder.is(PoiTypes.MEETING); + }); ++ private boolean isLobotomized = false; public boolean isLobotomized() { return this.isLobotomized; } // Purpur ++ private int notLobotomizedCount = 0; // Purpur + + public long nextGolemPanic = -1; // Pufferfish + +@@ -155,6 +157,90 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + this.setVillagerData(this.getVillagerData().setType(type).setProfession(VillagerProfession.NONE)); + } + ++ // Purpur start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.villagerRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.villagerRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.villagerControllable; ++ } ++ ++ @Override ++ protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); ++ if (level.purpurConfig.villagerFollowEmeraldBlock) this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, TEMPT_ITEMS, false)); ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.villagerMaxHealth); ++ } ++ ++ @Override ++ public boolean canBeLeashed(Player player) { ++ return level.purpurConfig.villagerCanBeLeashed && !this.isLeashed(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.villagerTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.villagerAlwaysDropExp; ++ } ++ ++ private boolean checkLobotomized() { ++ int interval = this.level.purpurConfig.villagerLobotomizeCheckInterval; ++ if (this.notLobotomizedCount > 3) { ++ // check half as often if not lobotomized for the last 3+ consecutive checks ++ interval *= 2; ++ } ++ if (this.level.getGameTime() % interval == 0) { ++ // offset Y for short blocks like dirt_path/farmland ++ this.isLobotomized = !canTravelFrom(BlockPos.containing(this.position().x, this.getBoundingBox().minY + 0.0625D, this.position().z)); ++ ++ if (this.isLobotomized) { ++ this.notLobotomizedCount = 0; ++ } else { ++ this.notLobotomizedCount++; ++ } ++ } ++ return this.isLobotomized; ++ } ++ ++ private boolean canTravelFrom(BlockPos pos) { ++ return canTravelTo(pos.east()) || canTravelTo(pos.west()) || canTravelTo(pos.north()) || canTravelTo(pos.south()); ++ } ++ ++ private boolean canTravelTo(BlockPos pos) { ++ net.minecraft.world.level.block.state.BlockState state = this.level.getBlockStateIfLoaded(pos); ++ if (state == null) { ++ // chunk not loaded ++ return false; ++ } ++ net.minecraft.world.level.block.Block bottom = state.getBlock(); ++ if (bottom instanceof net.minecraft.world.level.block.FenceBlock || ++ bottom instanceof net.minecraft.world.level.block.FenceGateBlock || ++ bottom instanceof net.minecraft.world.level.block.WallBlock) { ++ // bottom block is too tall to get over ++ return false; ++ } ++ net.minecraft.world.level.block.Block top = level.getBlockState(pos.above()).getBlock(); ++ // only if both blocks have no collision ++ return !bottom.hasCollision && !top.hasCollision; ++ } ++ // Purpur end ++ + @Override + public Brain getBrain() { + return (Brain) super.getBrain(); // CraftBukkit - decompile error +@@ -189,7 +275,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + brain.addActivity(Activity.PLAY, VillagerGoalPackages.getPlayPackage(0.5F)); + } else { + brain.setSchedule(Schedule.VILLAGER_DEFAULT); +- brain.addActivityWithConditions(Activity.WORK, VillagerGoalPackages.getWorkPackage(villagerprofession, 0.5F), ImmutableSet.of(Pair.of(MemoryModuleType.JOB_SITE, MemoryStatus.VALUE_PRESENT))); ++ brain.addActivityWithConditions(Activity.WORK, VillagerGoalPackages.getWorkPackage(villagerprofession, 0.5F, this.level.purpurConfig.villagerClericsFarmWarts), ImmutableSet.of(Pair.of(MemoryModuleType.JOB_SITE, MemoryStatus.VALUE_PRESENT))); // Purpur + } + + brain.addActivity(Activity.CORE, VillagerGoalPackages.getCorePackage(villagerprofession, 0.5F)); +@@ -249,14 +335,29 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + @Override + protected void customServerAiStep() { mobTick(false); } + protected void mobTick(boolean inactive) { +- this.level.getProfiler().push("villagerBrain"); ++ //this.level.getProfiler().push("villagerBrain"); // Purpur ++ // Purpur start ++ if (this.level.purpurConfig.villagerLobotomizeEnabled) { ++ // treat as inactive if lobotomized ++ inactive = inactive || checkLobotomized(); ++ } else { ++ // clean up state for API ++ this.isLobotomized = false; ++ } ++ // Purpur end + // Pufferfish start + if (!inactive) { +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel) this.level, this); // Paper + } + // Pufferfish end +- this.level.getProfiler().pop(); ++ // Purpur start ++ else if (this.isLobotomized && shouldRestock()) { ++ // make sure we restock if needed when lobotomized ++ restock(); ++ } ++ // Purpur end ++ //this.level.getProfiler().pop(); // Purpur + if (this.assignProfessionWhenSpawned) { + this.assignProfessionWhenSpawned = false; + } +@@ -312,7 +413,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + if (!itemstack.is(Items.VILLAGER_SPAWN_EGG) && this.isAlive() && !this.isTrading() && !this.isSleeping()) { + if (this.isBaby()) { + this.setUnhappy(); +- return InteractionResult.sidedSuccess(this.level.isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level.isClientSide)); // Purpur + } else { + boolean flag = this.getOffers().isEmpty(); + +@@ -325,9 +426,10 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + if (flag) { +- return InteractionResult.sidedSuccess(this.level.isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level.isClientSide)); // Purpur + } else { +- if (!this.level.isClientSide && !this.offers.isEmpty()) { ++ if (level.purpurConfig.villagerRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur ++ if (this.level.purpurConfig.villagerAllowTrading && !this.offers.isEmpty()) { // Purpur + this.startTrading(player); + } + +@@ -496,7 +598,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + while (iterator.hasNext()) { + MerchantOffer merchantrecipe = (MerchantOffer) iterator.next(); + +- merchantrecipe.updateDemand(); ++ merchantrecipe.updateDemand(this.level.purpurConfig.villagerMinimumDemand); // Purpur + } + + } +@@ -746,7 +848,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + @Override + public boolean canBreed() { +- return this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; ++ return this.level.purpurConfig.villagerCanBreed && this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; // Purpur + } + + private boolean hungry() { +@@ -938,6 +1040,11 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + public boolean hasFarmSeeds() { ++ // Purpur start ++ if (this.level.purpurConfig.villagerClericsFarmWarts && this.getVillagerData().getProfession() == VillagerProfession.CLERIC) { ++ return this.getInventory().hasAnyOf(ImmutableSet.of(Items.NETHER_WART)); ++ } ++ // Purpur end + return this.getInventory().hasAnyOf(ImmutableSet.of(Items.WHEAT_SEEDS, Items.POTATO, Items.CARROT, Items.BEETROOT_SEEDS)); + } + +@@ -986,6 +1093,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + } + + public void spawnGolemIfNeeded(ServerLevel world, long time, int requiredCount) { ++ if (world.purpurConfig.villagerSpawnIronGolemRadius > 0 && world.getEntitiesOfClass(net.minecraft.world.entity.animal.IronGolem.class, getBoundingBox().inflate(world.purpurConfig.villagerSpawnIronGolemRadius)).size() > world.purpurConfig.villagerSpawnIronGolemLimit) return; // Purpur + if (this.wantsToSpawnGolem(time)) { + AABB axisalignedbb = this.getBoundingBox().inflate(10.0D, 10.0D, 10.0D); + List list = world.getEntitiesOfClass(Villager.class, axisalignedbb); +@@ -1059,6 +1167,12 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + @Override + public void startSleeping(BlockPos pos) { ++ // Purpur start ++ if (level.purpurConfig.bedExplodeOnVillagerSleep && this.level.getBlockState(pos).getBlock() instanceof net.minecraft.world.level.block.BedBlock) { ++ this.level.explode(null, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (float) this.level.purpurConfig.bedExplosionPower, this.level.purpurConfig.bedExplosionFire, this.level.purpurConfig.bedExplosionEffect); ++ return; ++ } ++ // Purpur end + super.startSleeping(pos); + this.brain.setMemory(MemoryModuleType.LAST_SLEPT, this.level.getGameTime()); // CraftBukkit - decompile error + this.brain.eraseMemory(MemoryModuleType.WALK_TARGET); +diff --git a/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java b/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java +index ac70c2c03241e73943bd517a8c69dd05e0873634..0318663a824d2a9515f867a075d148c3fcb1a907 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java ++++ b/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java +@@ -26,7 +26,7 @@ public record VillagerProfession(String name, Predicate> heldJob + public static final VillagerProfession ARMORER = register("armorer", PoiTypes.ARMORER, SoundEvents.VILLAGER_WORK_ARMORER); + public static final VillagerProfession BUTCHER = register("butcher", PoiTypes.BUTCHER, SoundEvents.VILLAGER_WORK_BUTCHER); + public static final VillagerProfession CARTOGRAPHER = register("cartographer", PoiTypes.CARTOGRAPHER, SoundEvents.VILLAGER_WORK_CARTOGRAPHER); +- public static final VillagerProfession CLERIC = register("cleric", PoiTypes.CLERIC, SoundEvents.VILLAGER_WORK_CLERIC); ++ public static final VillagerProfession CLERIC = register("cleric", PoiTypes.CLERIC, ImmutableSet.of(Items.NETHER_WART), ImmutableSet.of(Blocks.SOUL_SAND), SoundEvents.VILLAGER_WORK_CLERIC); // Purpur + public static final VillagerProfession FARMER = register("farmer", PoiTypes.FARMER, ImmutableSet.of(Items.WHEAT, Items.WHEAT_SEEDS, Items.BEETROOT_SEEDS, Items.BONE_MEAL), ImmutableSet.of(Blocks.FARMLAND), SoundEvents.VILLAGER_WORK_FARMER); + public static final VillagerProfession FISHERMAN = register("fisherman", PoiTypes.FISHERMAN, SoundEvents.VILLAGER_WORK_FISHERMAN); + public static final VillagerProfession FLETCHER = register("fletcher", PoiTypes.FLETCHER, SoundEvents.VILLAGER_WORK_FLETCHER); +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java +index c9fb50c33ac15fe72bc77167e4647f30942fdc5d..a6a4d5203cb5f35306f8225e56681bc25e06beed 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java +@@ -69,6 +69,43 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + this.setDespawnDelay(48000); // CraftBukkit - set default from MobSpawnerTrader + } + ++ // Purpur - start ++ @Override ++ public boolean isRidable() { ++ return level.purpurConfig.wanderingTraderRidable; ++ } ++ ++ @Override ++ public boolean dismountsUnderwater() { ++ return level.purpurConfig.useDismountsUnderwaterTag ? super.dismountsUnderwater() : !level.purpurConfig.wanderingTraderRidableInWater; ++ } ++ ++ @Override ++ public boolean isControllable() { ++ return level.purpurConfig.wanderingTraderControllable; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.wanderingTraderMaxHealth); ++ } ++ ++ @Override ++ public boolean canBeLeashed(Player player) { ++ return level.purpurConfig.wanderingTraderCanBeLeashed && !this.isLeashed(); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level.purpurConfig.wanderingTraderTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level.purpurConfig.wanderingTraderAlwaysDropExp; ++ } ++ // Purpur end ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); +@@ -76,7 +113,7 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + return this.canDrinkPotion && this.level.isNight() && !entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API + })); + this.goalSelector.addGoal(0, new UseItemGoal<>(this, new ItemStack(Items.MILK_BUCKET), SoundEvents.WANDERING_TRADER_REAPPEARED, (entityvillagertrader) -> { +- return canDrinkMilk && this.level.isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API ++ return level.purpurConfig.milkClearsBeneficialEffects && canDrinkMilk && this.level.isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API // Purpur + })); + this.goalSelector.addGoal(1, new TradeWithPlayerGoal(this)); + this.goalSelector.addGoal(1, new AvoidEntityGoal<>(this, Zombie.class, 8.0F, 0.5D, 0.5D)); +@@ -89,6 +126,7 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + this.goalSelector.addGoal(1, new PanicGoal(this, 0.5D)); + this.goalSelector.addGoal(1, new LookAtTradingPlayerGoal(this)); + this.goalSelector.addGoal(2, new WanderingTrader.WanderToPositionGoal(this, 2.0D, 0.35D)); ++ if (level.purpurConfig.wanderingTraderFollowEmeraldBlock) this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, TEMPT_ITEMS, false)); // Purpur + this.goalSelector.addGoal(4, new MoveTowardsRestrictionGoal(this, 0.35D)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 0.35D)); + this.goalSelector.addGoal(9, new InteractGoal(this, Player.class, 3.0F, 1.0F)); +@@ -116,9 +154,10 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill + } + + if (this.getOffers().isEmpty()) { +- return InteractionResult.sidedSuccess(this.level.isClientSide); ++ return tryRide(player, hand, InteractionResult.sidedSuccess(this.level.isClientSide)); // Purpur + } else { +- if (!this.level.isClientSide) { ++ if (level.purpurConfig.wanderingTraderRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur ++ if (this.level.purpurConfig.wanderingTraderAllowTrading) { // Purpur + this.setTradingPlayer(player); + this.openTradingScreen(player, this.getDisplayName(), 1); + } +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +index 5d199fe497bd852827d3d18fb7566a09e70331a3..6cd8a50289a6404441e9e5e08d82d2ebe14a09cc 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +@@ -159,7 +159,17 @@ public class WanderingTraderSpawner implements CustomSpawner { + int k = pos.getX() + this.random.nextInt(range * 2) - range; + int l = pos.getZ() + this.random.nextInt(range * 2) - range; + int i1 = world.getHeight(Heightmap.Types.WORLD_SURFACE, k, l); +- BlockPos blockposition2 = new BlockPos(k, i1, l); ++ // Purpur start - allow traders to spawn below nether roof ++ BlockPos.MutableBlockPos blockposition2 = new BlockPos.MutableBlockPos(k, i1, l); ++ if (world.dimensionType().hasCeiling()) { ++ do { ++ blockposition2.relative(net.minecraft.core.Direction.DOWN); ++ } while (!world.getBlockState(blockposition2).isAir()); ++ do { ++ blockposition2.relative(net.minecraft.core.Direction.DOWN); ++ } while (world.getBlockState(blockposition2).isAir() && blockposition2.getY() > 0); ++ } ++ // Purpur end + + if (NaturalSpawner.isSpawnPositionOk(SpawnPlacements.Type.ON_GROUND, world, blockposition2, EntityType.WANDERING_TRADER)) { + blockposition1 = blockposition2; +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 0629c471d38a77c44fc1c86ccdfcb0690f61ca17..7edcb5b86f27d05a0526229262e0d3a3e160362b 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -186,6 +186,8 @@ public abstract class Player extends LivingEntity { + public boolean affectsSpawning = true; + public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; + // Paper end ++ public int sixRowEnderchestSlotCount = -1; // Purpur ++ public boolean canPortalInstant = false; // Purpur + + // CraftBukkit start + public boolean fauxSleeping; +@@ -197,6 +199,28 @@ public abstract class Player extends LivingEntity { + } + // CraftBukkit end + ++ // Purpur start ++ public int burpDelay = 0; ++ ++ public abstract void resetLastActionTime(); ++ ++ public void setAfk(boolean afk) { ++ } ++ ++ public boolean isAfk() { ++ return false; ++ } ++ ++ @Override ++ public boolean processClick(InteractionHand hand) { ++ Entity vehicle = getRootVehicle(); ++ if (vehicle != null && vehicle.getRider() == this) { ++ return vehicle.onClick(hand); ++ } ++ return false; ++ } ++ // Purpur end ++ + public Player(Level world, BlockPos pos, float yaw, GameProfile gameProfile) { + super(EntityType.PLAYER, world); + this.lastItemInMainHand = ItemStack.EMPTY; +@@ -241,6 +265,12 @@ public abstract class Player extends LivingEntity { + + @Override + public void tick() { ++ // Purpur start ++ if (this.burpDelay > 0 && --this.burpDelay == 0) { ++ this.level.playSound(null, getX(), getY(), getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 1.0F, this.level.random.nextFloat() * 0.1F + 0.9F); ++ } ++ // Purpur end ++ + this.noPhysics = this.isSpectator(); + if (this.isSpectator()) { + this.onGround = false; +@@ -344,6 +374,16 @@ public abstract class Player extends LivingEntity { + this.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200, 0, false, false, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.TURTLE_HELMET); // CraftBukkit + } + ++ // Purpur start ++ if (this.level.purpurConfig.playerNetheriteFireResistanceDuration > 0 && this.level.getGameTime() % 20 == 0) { ++ if (itemstack.is(Items.NETHERITE_HELMET) ++ && this.getItemBySlot(EquipmentSlot.CHEST).is(Items.NETHERITE_CHESTPLATE) ++ && this.getItemBySlot(EquipmentSlot.LEGS).is(Items.NETHERITE_LEGGINGS) ++ && this.getItemBySlot(EquipmentSlot.FEET).is(Items.NETHERITE_BOOTS)) { ++ this.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, this.level.purpurConfig.playerNetheriteFireResistanceDuration, this.level.purpurConfig.playerNetheriteFireResistanceAmplifier, this.level.purpurConfig.playerNetheriteFireResistanceAmbient, this.level.purpurConfig.playerNetheriteFireResistanceShowParticles, this.level.purpurConfig.playerNetheriteFireResistanceShowIcon), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.NETHERITE_ARMOR); ++ } ++ } ++ // Purpur end + } + + protected ItemCooldowns createItemCooldowns() { +@@ -430,7 +470,7 @@ public abstract class Player extends LivingEntity { + + @Override + public int getPortalWaitTime() { +- return this.abilities.invulnerable ? 1 : 80; ++ return canPortalInstant ? 1 : this.abilities.invulnerable ? this.level.purpurConfig.playerCreativePortalWaitTime : this.level.purpurConfig.playerPortalWaitTime; // Purpur + } + + @Override +@@ -590,7 +630,7 @@ public abstract class Player extends LivingEntity { + for (int i = 0; i < list.size(); ++i) { + Entity entity = (Entity) list.get(i); + +- if (entity.getType() == EntityType.EXPERIENCE_ORB) { ++ if (entity.getType() == EntityType.EXPERIENCE_ORB && entity.level.purpurConfig.playerExpPickupDelay >= 0) { // Purpur + list1.add(entity); + } else if (!entity.isRemoved()) { + this.touch(entity); +@@ -1281,7 +1321,7 @@ public abstract class Player extends LivingEntity { + flag2 = flag2 && !level.paperConfig().entities.behavior.disablePlayerCrits; // Paper + flag2 = flag2 && !this.isSprinting(); + if (flag2) { +- f *= 1.5F; ++ f *= this.level.purpurConfig.playerCriticalDamageMultiplier; // Purpur + } + + f += f1; +@@ -1950,9 +1990,19 @@ public abstract class Player extends LivingEntity { + @Override + public int getExperienceReward() { + if (!this.level.getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) && !this.isSpectator()) { +- int i = this.experienceLevel * 7; +- +- return i > 100 ? 100 : i; ++ // Purpur start ++ int toDrop; ++ try { ++ toDrop = Math.round(((Number) scriptEngine.eval("let expLevel = " + experienceLevel + "; " + ++ "let expTotal = " + totalExperience + "; " + ++ "let exp = " + experienceProgress + "; " + ++ level.purpurConfig.playerDeathExpDropEquation)).floatValue()); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ toDrop = experienceLevel * 7; ++ } ++ return Math.min(toDrop, level.purpurConfig.playerDeathExpDropMax); ++ // Purpur end + } else { + return 0; + } +@@ -2028,6 +2078,11 @@ public abstract class Player extends LivingEntity { + return this.inventory.armor; + } + ++ @Override ++ public boolean dismountsUnderwater() { ++ return !level.purpurConfig.playerRidableInWater; ++ } ++ + public boolean setEntityOnShoulder(CompoundTag entityNbt) { + if (!this.isPassenger() && this.onGround && !this.isInWater() && !this.isInPowderSnow) { + if (this.getShoulderEntityLeft().isEmpty()) { +@@ -2311,7 +2366,7 @@ public abstract class Player extends LivingEntity { + public ItemStack eat(Level world, ItemStack stack) { + this.getFoodData().eat(stack.getItem(), stack); + this.awardStat(Stats.ITEM_USED.get(stack.getItem())); +- world.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F); ++ // world.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F); // Purpur - moved to tick() + if (this instanceof ServerPlayer) { + CriteriaTriggers.CONSUME_ITEM.trigger((ServerPlayer) this, stack); + } +diff --git a/src/main/java/net/minecraft/world/entity/player/StackedContents.java b/src/main/java/net/minecraft/world/entity/player/StackedContents.java +index 574ebb3a2fcd0e4e426a8a7ee88d722ed3b9c3f5..842b921799111789b37a34b76644c9217bc85794 100644 +--- a/src/main/java/net/minecraft/world/entity/player/StackedContents.java ++++ b/src/main/java/net/minecraft/world/entity/player/StackedContents.java +@@ -37,8 +37,62 @@ public class StackedContents { + int i = getStackingIndex(stack); + int j = Math.min(maxCount, stack.getCount()); + this.put(i, j); ++ // PaperPR start ++ if (stack.hasTag()) { ++ this.put(getExactStackingIndex(stack), j); ++ } ++ } ++ ++ } ++ private static final net.minecraft.core.IdMap EXACT_MATCHES_ID_MAP = new net.minecraft.core.IdMap<>() { ++ private final java.util.concurrent.atomic.AtomicInteger idCounter = new java.util.concurrent.atomic.AtomicInteger(BuiltInRegistries.ITEM.size()); ++ private final it.unimi.dsi.fastutil.objects.Object2IntMap itemstackToId = new it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap<>(new it.unimi.dsi.fastutil.Hash.Strategy<>() { ++ @Override ++ public int hashCode(ItemStack o) { ++ return java.util.Objects.hash(o.getItem(), o.getTag()); ++ } ++ ++ @Override ++ public boolean equals(@Nullable ItemStack a, @Nullable ItemStack b) { ++ if (a == null || b == null) { ++ return false; ++ } ++ return ItemStack.tagMatches(a, b); ++ } ++ }); ++ private final it.unimi.dsi.fastutil.ints.Int2ObjectMap idToItemstack = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); ++ ++ @Override ++ public int getId(ItemStack value) { ++ if (!this.itemstackToId.containsKey(value)) { ++ final int id = this.idCounter.incrementAndGet(); ++ final ItemStack copy = value.copy(); ++ this.itemstackToId.put(copy, id); ++ this.idToItemstack.put(id, copy); ++ return id; ++ } ++ return this.itemstackToId.getInt(value); ++ } ++ ++ @Override ++ public @Nullable ItemStack byId(int index) { ++ return this.idToItemstack.get(index); ++ } ++ ++ @Override ++ public int size() { ++ return this.itemstackToId.size(); ++ } ++ ++ @Override ++ public java.util.Iterator iterator() { ++ return this.idToItemstack.values().iterator(); + } ++ }; + ++ public static int getExactStackingIndex(ItemStack stack) { ++ return EXACT_MATCHES_ID_MAP.getId(stack); ++ // PaperPR end + } + + public static int getStackingIndex(ItemStack stack) { +@@ -80,6 +134,12 @@ public class StackedContents { + } + + public static ItemStack fromStackingIndex(int itemId) { ++ // PaperPR start ++ if (itemId > BuiltInRegistries.ITEM.size()) { ++ final ItemStack stack = EXACT_MATCHES_ID_MAP.byId(itemId); ++ return stack == null ? ItemStack.EMPTY : stack.copy(); ++ } ++ // PaperPR end + return itemId == 0 ? ItemStack.EMPTY : new ItemStack(Item.byId(itemId)); + } + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +index 5d6d26cfe8f0ab68a3145214b3fc126ca7a71a66..c6fddfa199e0c42e0556ed1ad380885a17208e37 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +@@ -72,6 +72,7 @@ public abstract class AbstractArrow extends Projectile { + private IntOpenHashSet piercingIgnoreEntityIds; + @Nullable + private List piercedAndKilledEntities; ++ public int lootingLevel; // Purpur + + // Spigot Start + @Override +@@ -312,7 +313,7 @@ public abstract class AbstractArrow extends Projectile { + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(vec3d.multiply((double) (this.random.nextFloat() * 0.2F), (double) (this.random.nextFloat() * 0.2F), (double) (this.random.nextFloat() * 0.2F))); +- this.life = 0; ++ if (this.level.purpurConfig.arrowMovementResetsDespawnCounter) this.life = 0; // Purpur - do not reset despawn counter + } + + @Override +@@ -612,6 +613,12 @@ public abstract class AbstractArrow extends Projectile { + this.knockback = punch; + } + ++ // Purpur start ++ public void setLootingLevel(int looting) { ++ this.lootingLevel = looting; ++ } ++ // Purpur end ++ + public int getKnockback() { + return this.knockback; + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java b/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java +index 4daa368881e4fa59a9365d7b3810ae7dc1455fa3..a4041580061b2acd150836a1437df66ebb4a62cb 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/LargeFireball.java +@@ -16,20 +16,20 @@ public class LargeFireball extends Fireball { + + public LargeFireball(EntityType type, Level world) { + super(type, world); +- isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit ++ isIncendiary = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit // Purpur + } + + public LargeFireball(Level world, LivingEntity owner, double velocityX, double velocityY, double velocityZ, int explosionPower) { + super(EntityType.FIREBALL, owner, velocityX, velocityY, velocityZ, world); + this.explosionPower = explosionPower; +- isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit ++ isIncendiary = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit // Purpur + } + + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); + if (!this.level.isClientSide) { +- boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ boolean flag = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Purpur + + // CraftBukkit start - fire ExplosionPrimeEvent + ExplosionPrimeEvent event = new ExplosionPrimeEvent((org.bukkit.entity.Explosive) this.getBukkitEntity()); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +index c4f4a26e016eea744f587461af80461074d48303..10b109de5abc015b61a896d363ad37a006dff554 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +@@ -26,6 +26,12 @@ public class LlamaSpit extends Projectile { + this.setPos(owner.getX() - (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(owner.yBodyRot * 0.017453292F), owner.getEyeY() - 0.10000000149011612D, owner.getZ() + (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(owner.yBodyRot * 0.017453292F)); + } + ++ // Purpur start ++ public void super_tick() { ++ super.tick(); ++ } ++ // Purpur end ++ + @Override + public void tick() { + super.tick(); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index e6f87e1e3c99195ed11c81162cb54e7f5750c4ba..220690cbd6552b06626f4edf5c71bed5cf1f12c4 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -307,6 +307,6 @@ public abstract class Projectile extends Entity implements TraceableEntity { + public boolean mayInteract(Level world, BlockPos pos) { + Entity entity = this.getOwner(); + +- return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.purpurConfig.projectilesBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +index b9e4955fecabbad8d6762f3d933ea1402e932d9b..895c501f8579799139239701703078ef060cd481 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +@@ -24,7 +24,7 @@ public class SmallFireball extends Fireball { + super(EntityType.SMALL_FIREBALL, owner, velocityX, velocityY, velocityZ, world); + // CraftBukkit start + if (this.getOwner() != null && this.getOwner() instanceof Mob) { +- isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ isIncendiary = this.level.purpurConfig.fireballsBypassMobGriefing || this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Purpur + } + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +index 6cded52e4627c2b6073fa221fc6d6583f1b2a96d..9a84e8cc1d1b2803a061fe9ef6297c9c4aea34c5 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +@@ -53,10 +53,40 @@ public class Snowball extends ThrowableItemProjectile { + protected void onHitEntity(EntityHitResult entityHitResult) { + super.onHitEntity(entityHitResult); + Entity entity = entityHitResult.getEntity(); +- int i = entity instanceof Blaze ? 3 : 0; ++ int i = entity.level.purpurConfig.snowballDamage >= 0 ? entity.level.purpurConfig.snowballDamage : entity instanceof Blaze ? 3 : 0; // Purpur + entity.hurt(this.damageSources().thrown(this, this.getOwner()), (float)i); + } + ++ // Purpur start - borrowed and modified code from ThrownPotion#onHitBlock and ThrownPotion#dowseFire ++ @Override ++ protected void onHitBlock(net.minecraft.world.phys.BlockHitResult blockHitResult) { ++ super.onHitBlock(blockHitResult); ++ ++ if (!this.level.isClientSide) { ++ net.minecraft.core.BlockPos blockposition = blockHitResult.getBlockPos(); ++ net.minecraft.core.BlockPos blockposition1 = blockposition.relative(blockHitResult.getDirection()); ++ ++ net.minecraft.world.level.block.state.BlockState iblockdata = this.level.getBlockState(blockposition); ++ ++ if (this.level.purpurConfig.snowballExtinguishesFire && this.level.getBlockState(blockposition1).is(net.minecraft.world.level.block.Blocks.FIRE)) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition1, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState()).isCancelled()) { ++ this.level.removeBlock(blockposition1, false); ++ } ++ } else if (this.level.purpurConfig.snowballExtinguishesCandles && net.minecraft.world.level.block.AbstractCandleBlock.isLit(iblockdata)) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.setValue(net.minecraft.world.level.block.AbstractCandleBlock.LIT, false)).isCancelled()) { ++ net.minecraft.world.level.block.AbstractCandleBlock.extinguish(null, iblockdata, this.level, blockposition); ++ } ++ } else if (this.level.purpurConfig.snowballExtinguishesCampfires && net.minecraft.world.level.block.CampfireBlock.isLitCampfire(iblockdata)) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.setValue(net.minecraft.world.level.block.CampfireBlock.LIT, false)).isCancelled()) { ++ this.level.levelEvent(null, 1009, blockposition, 0); ++ net.minecraft.world.level.block.CampfireBlock.dowse(this.getOwner(), this.level, blockposition, iblockdata); ++ this.level.setBlockAndUpdate(blockposition, iblockdata.setValue(net.minecraft.world.level.block.CampfireBlock.LIT, false)); ++ } ++ } ++ } ++ } ++ // Purpur end ++ + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index 39ab9a283d856ba8d578d1378285758e32a24cf0..f35547c6b5951bc6eb4df74b2a94496fd20d69b5 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -68,10 +68,11 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + Bukkit.getPluginManager().callEvent(teleEvent); + + if (!teleEvent.isCancelled() && entityplayer.connection.isAcceptingMessages()) { +- if (this.random.nextFloat() < 0.05F && this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { ++ if (this.random.nextFloat() < this.level.purpurConfig.enderPearlEndermiteChance && this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { // Purpur + Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(this.level); + + if (entityendermite != null) { ++ entityendermite.setPlayerSpawned(true); // Purpur + entityendermite.moveTo(entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot()); + this.level.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); + } +@@ -84,7 +85,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + entityplayer.connection.teleport(teleEvent.getTo()); + entity.resetFallDistance(); + CraftEventFactory.entityDamage = this; +- entity.hurt(this.damageSources().fall(), 5.0F); ++ entity.hurt(this.damageSources().fall(), this.level.purpurConfig.enderPearlDamage); // Purpur + CraftEventFactory.entityDamage = null; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java +index db151bf624095014c99d78b4f6748d2c3792abea..fb10fc5a5aa3b6d6220694041778bfd39ffa1cb8 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownTrident.java +@@ -60,7 +60,7 @@ public class ThrownTrident extends AbstractArrow { + Entity entity = this.getOwner(); + byte b0 = (Byte) this.entityData.get(ThrownTrident.ID_LOYALTY); + +- if (b0 > 0 && (this.dealtDamage || this.isNoPhysics()) && entity != null) { ++ if (b0 > 0 && (this.dealtDamage || this.isNoPhysics() || (level.purpurConfig.tridentLoyaltyVoidReturnHeight < 0.0D && getY() < level.purpurConfig.tridentLoyaltyVoidReturnHeight)) && entity != null) { // Purpur + if (!this.isAcceptibleReturnOwner()) { + if (!this.level.isClientSide && this.pickup == AbstractArrow.Pickup.ALLOWED) { + this.spawnAtLocation(this.getPickupItem(), 0.1F); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +index 093a00e52062868b4fbf358b307513d0f599f69d..ba753735f3cbb2cb3d0a491d1bd94a04f83b123d 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +@@ -95,7 +95,7 @@ public class WitherSkull extends AbstractHurtingProjectile { + if (!this.level.isClientSide) { + // CraftBukkit start + // this.level.explode(this, this.getX(), this.getY(), this.getZ(), 1.0F, false, World.a.MOB); +- ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), 1.0F, false); ++ ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), this.level.purpurConfig.witherExplosionRadius, false); // Purpur + this.level.getCraftServer().getPluginManager().callEvent(event); + + if (!event.isCancelled()) { +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java +index 99e9d46d42ddd0b451c6aeb847f1b295ebe5c697..f7b03dc1d5316aeb760a52d6f6c50e8aae5f978e 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raider.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java +@@ -319,7 +319,7 @@ public abstract class Raider extends PatrollingMonster { + + @Override + public boolean canUse() { +- if (!this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items ++ if ((!this.mob.level.purpurConfig.pillagerBypassMobGriefing && !this.mob.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items // Purpur + Raid raid = this.mob.getCurrentRaid(); + + if (this.mob.hasActiveRaid() && !this.mob.getCurrentRaid().isOver() && this.mob.canBeLeader() && !ItemStack.matches(this.mob.getItemBySlot(EquipmentSlot.HEAD), Raid.getLeaderBannerInstance())) { +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raids.java b/src/main/java/net/minecraft/world/entity/raid/Raids.java +index fabce3bc592b1b172b227395a07febdbb66ec3c9..df48bcc8f329e3855bb7426bdfe0e3c72af53bea 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raids.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java +@@ -28,6 +28,7 @@ import net.minecraft.world.phys.Vec3; + public class Raids extends SavedData { + + private static final String RAID_FILE_ID = "raids"; ++ public final Map playerCooldowns = Maps.newHashMap(); + public final Map raidMap = Maps.newHashMap(); + private final ServerLevel level; + private int nextAvailableID; +@@ -45,6 +46,17 @@ public class Raids extends SavedData { + + public void tick() { + ++this.tick; ++ // Purpur start ++ if (level.purpurConfig.raidCooldownSeconds != 0 && this.tick % 20 == 0) { ++ com.google.common.collect.ImmutableMap.copyOf(playerCooldowns).forEach((uuid, i) -> { ++ if (i < 1) { ++ playerCooldowns.remove(uuid); ++ } else { ++ playerCooldowns.put(uuid, i - 1); ++ } ++ }); ++ } ++ // Purpur end + Iterator iterator = this.raidMap.values().iterator(); + + while (iterator.hasNext()) { +@@ -129,11 +141,13 @@ public class Raids extends SavedData { + } + + if (flag) { ++ if (level.purpurConfig.raidCooldownSeconds != 0 && playerCooldowns.containsKey(player.getUUID())) return null; // Purpur + // CraftBukkit start + if (!org.bukkit.craftbukkit.event.CraftEventFactory.callRaidTriggerEvent(raid, player)) { + player.removeEffect(MobEffects.BAD_OMEN); + return null; + } ++ if (level.purpurConfig.raidCooldownSeconds != 0) playerCooldowns.put(player.getUUID(), level.purpurConfig.raidCooldownSeconds); // Purpur + + if (!this.raidMap.containsKey(raid.getId())) { + this.raidMap.put(raid.getId(), raid); +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java +index ee4f924afe15c9a4d96af7a55b357076c7b28501..ddcd7ada9f4bf5653bd8bf7c6cdb35d7243367d9 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java +@@ -107,11 +107,13 @@ public abstract class AbstractMinecart extends Entity { + private double flyingY = 0.949999988079071D; // Paper - restore vanilla precision + private double flyingZ = 0.949999988079071D; // Paper - restore vanilla precision + public double maxSpeed = 0.4D; ++ public double storedMaxSpeed; // Purpur + // CraftBukkit end + + protected AbstractMinecart(EntityType type, Level world) { + super(type, world); + this.blocksBuilding = true; ++ if (world != null) maxSpeed = storedMaxSpeed = world.purpurConfig.minecartMaxSpeed; // Purpur + } + + protected AbstractMinecart(EntityType type, Level world, double x, double y, double z) { +@@ -334,6 +336,12 @@ public abstract class AbstractMinecart extends Entity { + + @Override + public void tick() { ++ // Purpur start ++ if (storedMaxSpeed != level.purpurConfig.minecartMaxSpeed) { ++ maxSpeed = storedMaxSpeed = level.purpurConfig.minecartMaxSpeed; ++ } ++ // Purpur end ++ + // CraftBukkit start + double prevX = this.getX(); + double prevY = this.getY(); +@@ -497,16 +505,63 @@ public abstract class AbstractMinecart extends Entity { + + public void activateMinecart(int x, int y, int z, boolean powered) {} + ++ // Purpur start ++ private Double lastSpeed; ++ ++ public double getControllableSpeed() { ++ BlockPos pos = new BlockPos(this); ++ Block block = level.getBlockState(pos).getBlock(); ++ if (!block.material.isSolid()) { ++ block = level.getBlockState(pos.relative(Direction.DOWN)).getBlock(); ++ } ++ Double speed = level.purpurConfig.minecartControllableBlockSpeeds.get(block); ++ if (!block.material.isSolid()) { ++ speed = lastSpeed; ++ } ++ if (speed == null) { ++ speed = level.purpurConfig.minecartControllableBaseSpeed; ++ } ++ return lastSpeed = speed; ++ } ++ // Purpur end ++ + protected void comeOffTrack() { + double d0 = this.getMaxSpeed(); + Vec3 vec3d = this.getDeltaMovement(); + + this.setDeltaMovement(Mth.clamp(vec3d.x, -d0, d0), vec3d.y, Mth.clamp(vec3d.z, -d0, d0)); ++ ++ // Purpur start ++ if (level.purpurConfig.minecartControllable && !isInWater() && !isInLava() && !passengers.isEmpty()) { ++ Entity passenger = passengers.get(0); ++ if (passenger instanceof Player) { ++ Player player = (Player) passenger; ++ if (player.jumping && this.onGround) { ++ setDeltaMovement(new Vec3(getDeltaMovement().x, level.purpurConfig.minecartControllableHopBoost, getDeltaMovement().z)); ++ } ++ if (player.zza != 0.0F) { ++ Vector velocity = player.getBukkitEntity().getEyeLocation().getDirection().normalize().multiply(getControllableSpeed()); ++ if (player.zza < 0.0) { ++ velocity.multiply(-0.5); ++ } ++ setDeltaMovement(new Vec3(velocity.getX(), getDeltaMovement().y, velocity.getZ())); ++ } ++ this.setYRot(passenger.getYRot() - 90); ++ maxUpStep = level.purpurConfig.minecartControllableStepHeight; ++ } else { ++ maxUpStep = 0.0F; ++ } ++ } else { ++ maxUpStep = 0.0F; ++ } ++ // Purpur end ++ + if (this.onGround) { + // CraftBukkit start - replace magic numbers with our variables + this.setDeltaMovement(new Vec3(this.getDeltaMovement().x * this.derailedX, this.getDeltaMovement().y * this.derailedY, this.getDeltaMovement().z * this.derailedZ)); + // CraftBukkit end + } ++ else if (level.purpurConfig.minecartControllable) setDeltaMovement(new Vec3(getDeltaMovement().x * derailedX, getDeltaMovement().y, getDeltaMovement().z * derailedZ)); // Purpur + + this.move(MoverType.SELF, this.getDeltaMovement()); + if (!this.onGround) { +@@ -668,7 +723,7 @@ public abstract class AbstractMinecart extends Entity { + if (d18 > 0.01D) { + double d20 = 0.06D; + +- this.setDeltaMovement(vec3d4.add(vec3d4.x / d18 * 0.06D, 0.0D, vec3d4.z / d18 * 0.06D)); ++ this.setDeltaMovement(vec3d4.add(vec3d4.x / d18 * this.level.purpurConfig.poweredRailBoostModifier, 0.0D, vec3d4.z / d18 * this.level.purpurConfig.poweredRailBoostModifier)); // Purpur + } else { + Vec3 vec3d5 = this.getDeltaMovement(); + double d21 = vec3d5.x; +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +index 12e3209c5246ede89daaf8455fe70b4a517e12f6..06421017e3a7a0511c253e2ad4a028b0c156c9a8 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +@@ -223,7 +223,13 @@ public class Boat extends Entity implements VariantHolder { + } + + protected void destroy(DamageSource source) { +- this.spawnAtLocation((ItemLike) this.getDropItem()); ++ // Purpur start ++ final ItemStack boat = new ItemStack(this.getDropItem()); ++ if (this.level.purpurConfig.persistentDroppableEntityDisplayNames && this.hasCustomName()) { ++ boat.setHoverName(this.getCustomName()); ++ } ++ this.spawnAtLocation(boat); ++ // Purpur end + } + + @Override +@@ -541,6 +547,7 @@ public class Boat extends Entity implements VariantHolder { + + if (f > 0.0F) { + this.landFriction = f; ++ if (level.purpurConfig.boatEjectPlayersOnLand) ejectPassengers(); // Purpur + return Boat.Status.ON_LAND; + } else { + return Boat.Status.IN_AIR; +diff --git a/src/main/java/net/minecraft/world/food/FoodData.java b/src/main/java/net/minecraft/world/food/FoodData.java +index 4a2dcf9bd83dd3fdff43483f887f4f58dc4715cd..3d2e3c7dd3bf5c8f483a933e9f6868586c0d3911 100644 +--- a/src/main/java/net/minecraft/world/food/FoodData.java ++++ b/src/main/java/net/minecraft/world/food/FoodData.java +@@ -33,8 +33,10 @@ public class FoodData { + // CraftBukkit end + + public void eat(int food, float saturationModifier) { ++ int oldValue = this.foodLevel; // Purpur + this.foodLevel = Math.min(food + this.foodLevel, 20); + this.saturationLevel = Math.min(this.saturationLevel + (float) food * saturationModifier * 2.0F, (float) this.foodLevel); ++ if (this.entityhuman.level.purpurConfig.playerBurpWhenFull && this.foodLevel == 20 && oldValue < 20) this.entityhuman.burpDelay = this.entityhuman.level.purpurConfig.playerBurpDelay; // Purpur + } + + public void eat(Item item, ItemStack stack) { +@@ -100,7 +102,7 @@ public class FoodData { + ++this.tickTimer; + if (this.tickTimer >= this.starvationRate) { // CraftBukkit - add regen rate manipulation + if (player.getHealth() > 10.0F || enumdifficulty == Difficulty.HARD || player.getHealth() > 1.0F && enumdifficulty == Difficulty.NORMAL) { +- player.hurt(player.damageSources().starve(), 1.0F); ++ player.hurt(player.damageSources().starve(), player.level.purpurConfig.hungerStarvationDamage); // Purpur + } + + this.tickTimer = 0; +diff --git a/src/main/java/net/minecraft/world/food/FoodProperties.java b/src/main/java/net/minecraft/world/food/FoodProperties.java +index 9967ba762567631f2bdb1e4f8fe16a13ea927b46..6c945ae8fe8b1517e312c688f829fab41f12d9f4 100644 +--- a/src/main/java/net/minecraft/world/food/FoodProperties.java ++++ b/src/main/java/net/minecraft/world/food/FoodProperties.java +@@ -2,15 +2,22 @@ package net.minecraft.world.food; + + import com.google.common.collect.Lists; + import com.mojang.datafixers.util.Pair; ++ ++import java.util.ArrayList; + import java.util.List; + import net.minecraft.world.effect.MobEffectInstance; + + public class FoodProperties { +- private final int nutrition; +- private final float saturationModifier; +- private final boolean isMeat; +- private final boolean canAlwaysEat; +- private final boolean fastFood; ++ // Purpur start ++ private int nutrition; public void setNutrition(int nutrition) { this.nutrition = nutrition; } ++ private float saturationModifier; public void setSaturationModifier(float saturation) { this.saturationModifier = saturation; } ++ private boolean isMeat; public void setIsMeat(boolean isMeat) { this.isMeat = isMeat; } ++ private boolean canAlwaysEat; public void setCanAlwaysEat(boolean canAlwaysEat) { this.canAlwaysEat = canAlwaysEat; } ++ private boolean fastFood; public void setFastFood(boolean isFastFood) { this.fastFood = isFastFood; } ++ public FoodProperties copy() { ++ return new FoodProperties(this.nutrition, this.saturationModifier, this.isMeat, this.canAlwaysEat, this.fastFood, new ArrayList<>(this.effects)); ++ } ++ // Purpur end + private final List> effects; + + FoodProperties(int hunger, float saturationModifier, boolean meat, boolean alwaysEdible, boolean snack, List> statusEffects) { +diff --git a/src/main/java/net/minecraft/world/food/Foods.java b/src/main/java/net/minecraft/world/food/Foods.java +index b16d9e2eaa589f19c563ee70b1a56d67dbcdecb0..71beab673f04cd051c46ea37f8c847316885d38d 100644 +--- a/src/main/java/net/minecraft/world/food/Foods.java ++++ b/src/main/java/net/minecraft/world/food/Foods.java +@@ -4,6 +4,9 @@ import net.minecraft.world.effect.MobEffectInstance; + import net.minecraft.world.effect.MobEffects; + + public class Foods { ++ public static final java.util.Map ALL_PROPERTIES = new java.util.HashMap<>(); // Purpur ++ public static final java.util.Map DEFAULT_PROPERTIES = new java.util.HashMap<>(); // Purpur ++ + public static final FoodProperties APPLE = (new FoodProperties.Builder()).nutrition(4).saturationMod(0.3F).build(); + public static final FoodProperties BAKED_POTATO = (new FoodProperties.Builder()).nutrition(5).saturationMod(0.6F).build(); + public static final FoodProperties BEEF = (new FoodProperties.Builder()).nutrition(3).saturationMod(0.3F).meat().build(); +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +index c84908095a93d42826b21bf5f3490410fb0a5708..20b328704981c088597359fe18c1d67c339c1c0f 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -76,6 +76,7 @@ public abstract class AbstractContainerMenu { + @Nullable + private ContainerSynchronizer synchronizer; + private boolean suppressRemoteUpdates; ++ @javax.annotation.Nullable protected ItemStack activeQuickItem = null; // Purpur + + // CraftBukkit start + public boolean checkReachable = true; +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java +index 8ae78ae54d87ea7789df754311fa0e8aade0ce91..35479019fb846573f4e2eb5902f3ebe898c4d61f 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractFurnaceMenu.java +@@ -145,7 +145,13 @@ public abstract class AbstractFurnaceMenu extends RecipeBookMenu { + } else if (slot != 1 && slot != 0) { + if (this.canSmelt(itemstack1)) { + if (!this.moveItemStackTo(itemstack1, 0, 1, false)) { +- return ItemStack.EMPTY; ++ // Purpur start - fix #625 ++ if (this.isFuel(itemstack1)) { ++ if (!this.moveItemStackTo(itemstack1, 1, 2, false)) { ++ return ItemStack.EMPTY; ++ } ++ } ++ // Purpur end + } + } else if (this.isFuel(itemstack1)) { + if (!this.moveItemStackTo(itemstack1, 1, 2, false)) { +diff --git a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java +index b7a2295290227045e6426ee0f71707185d95b943..7ade5cd93b3d7b7259bf3246a8b74b0b31407817 100644 +--- a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java +@@ -21,6 +21,13 @@ import org.slf4j.Logger; + import org.bukkit.craftbukkit.inventory.CraftInventoryView; + // CraftBukkit end + ++// Purpur start ++import net.minecraft.nbt.IntTag; ++import net.minecraft.network.protocol.game.ClientboundContainerSetDataPacket; ++import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; ++import net.minecraft.server.level.ServerPlayer; ++// Purpur end ++ + public class AnvilMenu extends ItemCombinerMenu { + + public static final int INPUT_SLOT = 0; +@@ -48,6 +55,8 @@ public class AnvilMenu extends ItemCombinerMenu { + public int maximumRepairCost = 40; + private CraftInventoryView bukkitEntity; + // CraftBukkit end ++ public boolean bypassCost = false; // Purpur ++ public boolean canDoUnsafeEnchants = false; // Purpur + + public AnvilMenu(int syncId, Inventory inventory) { + this(syncId, inventory, ContainerLevelAccess.NULL); +@@ -75,12 +84,15 @@ public class AnvilMenu extends ItemCombinerMenu { + + @Override + protected boolean mayPickup(Player player, boolean present) { +- return (player.getAbilities().instabuild || player.experienceLevel >= this.cost.get()) && this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST && present; // CraftBukkit - allow cost 0 like a free item ++ return (player.getAbilities().instabuild || player.experienceLevel >= this.cost.get()) && (bypassCost || this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST) && present; // CraftBukkit - allow cost 0 like a free item // Purpur + } + + @Override + protected void onTake(Player player, ItemStack stack) { ++ ItemStack itemstack = activeQuickItem == null ? stack : activeQuickItem; // Purpur ++ if (org.purpurmc.purpur.event.inventory.AnvilTakeResultEvent.getHandlerList().getRegisteredListeners().length > 0) new org.purpurmc.purpur.event.inventory.AnvilTakeResultEvent(player.getBukkitEntity(), getBukkitView(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)).callEvent(); // Purpur + if (!player.getAbilities().instabuild) { ++ if (bypassCost) ((ServerPlayer) player).lastSentExp = -1; else // Purpur + player.giveExperienceLevels(-this.cost.get()); + } + +@@ -131,6 +143,12 @@ public class AnvilMenu extends ItemCombinerMenu { + + @Override + public void createResult() { ++ // Purpur start ++ bypassCost = false; ++ canDoUnsafeEnchants = false; ++ if (org.purpurmc.purpur.event.inventory.AnvilUpdateResultEvent.getHandlerList().getRegisteredListeners().length > 0) new org.purpurmc.purpur.event.inventory.AnvilUpdateResultEvent(getBukkitView()).callEvent(); ++ // Purpur end ++ + ItemStack itemstack = this.inputSlots.getItem(0); + + this.cost.set(1); +@@ -207,7 +225,8 @@ public class AnvilMenu extends ItemCombinerMenu { + int i2 = (Integer) map1.get(enchantment); + + i2 = l1 == i2 ? i2 + 1 : Math.max(i2, l1); +- boolean flag3 = enchantment.canEnchant(itemstack); ++ boolean flag3 = canDoUnsafeEnchants || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants && org.purpurmc.purpur.PurpurConfig.allowInapplicableEnchants) || enchantment.canEnchant(itemstack); // Purpur ++ boolean flag4 = true; // Purpur + + if (this.player.getAbilities().instabuild || itemstack.is(Items.ENCHANTED_BOOK)) { + flag3 = true; +@@ -219,16 +238,16 @@ public class AnvilMenu extends ItemCombinerMenu { + Enchantment enchantment1 = (Enchantment) iterator1.next(); + + if (enchantment1 != enchantment && !enchantment.isCompatibleWith(enchantment1)) { +- flag3 = false; ++ flag4 = canDoUnsafeEnchants || (org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants && org.purpurmc.purpur.PurpurConfig.allowIncompatibleEnchants); // Purpur flag3 -> flag4 + ++i; + } + } + +- if (!flag3) { ++ if (!flag3 || !flag4) { // Purpur + flag2 = true; + } else { + flag1 = true; +- if (i2 > enchantment.getMaxLevel()) { ++ if ((!org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants || !org.purpurmc.purpur.PurpurConfig.allowHigherEnchantsLevels) && i2 > enchantment.getMaxLevel()) { // Purpur + i2 = enchantment.getMaxLevel(); + } + +@@ -278,6 +297,54 @@ public class AnvilMenu extends ItemCombinerMenu { + } else if (!this.itemName.equals(itemstack.getHoverName().getString())) { + b1 = 1; + i += b1; ++ // Purpur start ++ if (this.player != null) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity player = this.player.getBukkitEntity(); ++ String name = this.itemName; ++ boolean removeItalics = false; ++ if (player.hasPermission("purpur.anvil.remove_italics")) { ++ if (name.startsWith("&r")) { ++ name = name.substring(2); ++ removeItalics = true; ++ } else if (name.startsWith("")) { ++ name = name.substring(3); ++ removeItalics = true; ++ } else if (name.startsWith("")) { ++ name = name.substring(7); ++ removeItalics = true; ++ } ++ } ++ if (this.player.level.purpurConfig.anvilAllowColors) { ++ if (player.hasPermission("purpur.anvil.color")) { ++ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(?i)&([0-9a-fr])").matcher(name); ++ while (matcher.find()) { ++ String match = matcher.group(1); ++ name = name.replace("&" + match, "\u00a7" + match.toLowerCase(java.util.Locale.ROOT)); ++ } ++ //name = name.replaceAll("(?i)&([0-9a-fr])", "\u00a7$1"); ++ } ++ if (player.hasPermission("purpur.anvil.format")) { ++ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(?i)&([k-or])").matcher(name); ++ while (matcher.find()) { ++ String match = matcher.group(1); ++ name = name.replace("&" + match, "\u00a7" + match.toLowerCase(java.util.Locale.ROOT)); ++ } ++ //name = name.replaceAll("(?i)&([l-or])", "\u00a7$1"); ++ } ++ } ++ net.kyori.adventure.text.Component component; ++ if (this.player.level.purpurConfig.anvilColorsUseMiniMessage && player.hasPermission("purpur.anvil.minimessage")) { ++ component = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(org.bukkit.ChatColor.stripColor(name)); ++ } else { ++ component = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(name); ++ } ++ if (removeItalics) { ++ component = component.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ itemstack1.setHoverName(io.papermc.paper.adventure.PaperAdventure.asVanilla(component)); ++ } ++ else ++ // Purpur end + itemstack1.setHoverName(Component.literal(this.itemName)); + } + +@@ -290,6 +357,13 @@ public class AnvilMenu extends ItemCombinerMenu { + this.cost.set(this.maximumRepairCost - 1); // CraftBukkit + } + ++ // Purpur start ++ if (bypassCost && cost.get() >= maximumRepairCost) { ++ itemstack.addTagElement("Purpur.realCost", IntTag.valueOf(cost.get())); ++ cost.set(maximumRepairCost - 1); ++ } ++ // Purpur end ++ + if (this.cost.get() >= this.maximumRepairCost && !this.player.getAbilities().instabuild) { // CraftBukkit + itemstack1 = ItemStack.EMPTY; + } +@@ -312,11 +386,17 @@ public class AnvilMenu extends ItemCombinerMenu { + org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), itemstack1); // CraftBukkit + sendAllDataToRemote(); // CraftBukkit - SPIGOT-6686: Always send completed inventory to stay in sync with client + this.broadcastChanges(); ++ // Purpur start ++ if ((canDoUnsafeEnchants || org.purpurmc.purpur.PurpurConfig.allowUnsafeEnchants) && itemstack1 != ItemStack.EMPTY) { ++ ((ServerPlayer) player).connection.send(new ClientboundContainerSetSlotPacket(containerId, incrementStateId(), 2, itemstack1)); ++ ((ServerPlayer) player).connection.send(new ClientboundContainerSetDataPacket(containerId, 0, cost.get())); ++ } ++ // Purpur end + } + } + + public static int calculateIncreasedRepairCost(int cost) { +- return cost * 2 + 1; ++ return org.purpurmc.purpur.PurpurConfig.anvilCumulativeCost ? cost * 2 + 1 : 0; + } + + public void setItemName(String newItemName) { +diff --git a/src/main/java/net/minecraft/world/inventory/ChestMenu.java b/src/main/java/net/minecraft/world/inventory/ChestMenu.java +index 0dbfd23bbfc6ad203f048142f8c90ef741849fe1..9a80427d2bb470b6b1638e59aba57216676dcbd2 100644 +--- a/src/main/java/net/minecraft/world/inventory/ChestMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/ChestMenu.java +@@ -67,10 +67,30 @@ public class ChestMenu extends AbstractContainerMenu { + return new ChestMenu(MenuType.GENERIC_9x6, syncId, playerInventory, 6); + } + ++ // Purpur start ++ public static ChestMenu oneRow(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x1, syncId, playerInventory, inventory, 1); ++ } ++ ++ public static ChestMenu twoRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x2, syncId, playerInventory, inventory, 2); ++ } ++ // Purpur end ++ + public static ChestMenu threeRows(int syncId, Inventory playerInventory, Container inventory) { + return new ChestMenu(MenuType.GENERIC_9x3, syncId, playerInventory, inventory, 3); + } + ++ // Purpur start ++ public static ChestMenu fourRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x4, syncId, playerInventory, inventory, 4); ++ } ++ ++ public static ChestMenu fiveRows(int syncId, Inventory playerInventory, Container inventory) { ++ return new ChestMenu(MenuType.GENERIC_9x5, syncId, playerInventory, inventory, 5); ++ } ++ // Purpur end ++ + public static ChestMenu sixRows(int syncId, Inventory playerInventory, Container inventory) { + return new ChestMenu(MenuType.GENERIC_9x6, syncId, playerInventory, inventory, 6); + } +diff --git a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +index c2fc00509bf3690d359928e8d352d4b3c2ca1491..69ae671be07b1928e778399551991777829e432a 100644 +--- a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +@@ -38,6 +38,12 @@ import org.bukkit.event.enchantment.PrepareItemEnchantEvent; + import org.bukkit.entity.Player; + // CraftBukkit end + ++// Purpur start ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.EnchantmentTableBlockEntity; ++import org.bukkit.craftbukkit.entity.CraftHumanEntity; ++// Purpur end ++ + public class EnchantmentMenu extends AbstractContainerMenu { + + private final Container enchantSlots; +@@ -71,6 +77,22 @@ public class EnchantmentMenu extends AbstractContainerMenu { + return context.getLocation(); + } + // CraftBukkit end ++ ++ // Purpur start ++ @Override ++ public void onClose(CraftHumanEntity who) { ++ super.onClose(who); ++ ++ if (who.getHandle().getLevel().purpurConfig.enchantmentTableLapisPersists) { ++ access.execute((level, pos) -> { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ if (blockEntity instanceof EnchantmentTableBlockEntity enchantmentTable) { ++ enchantmentTable.setLapis(this.getItem(1).getCount()); ++ } ++ }); ++ } ++ } ++ // Purpur end + }; + this.random = RandomSource.create(); + this.enchantmentSeed = DataSlot.standalone(); +@@ -96,6 +118,17 @@ public class EnchantmentMenu extends AbstractContainerMenu { + } + }); + ++ // Purpur start ++ access.execute((level, pos) -> { ++ if (level.purpurConfig.enchantmentTableLapisPersists) { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ if (blockEntity instanceof EnchantmentTableBlockEntity enchantmentTable) { ++ this.getSlot(1).set(new ItemStack(Items.LAPIS_LAZULI, enchantmentTable.getLapis())); ++ } ++ } ++ }); ++ // Purpur end ++ + int j; + + for (j = 0; j < 3; ++j) { +@@ -338,6 +371,7 @@ public class EnchantmentMenu extends AbstractContainerMenu { + public void removed(net.minecraft.world.entity.player.Player player) { + super.removed(player); + this.access.execute((world, blockposition) -> { ++ if (world.purpurConfig.enchantmentTableLapisPersists) this.getSlot(1).set(ItemStack.EMPTY); // Purpur + this.clearContainer(player, this.enchantSlots); + }); + } +diff --git a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java +index 89838076f3231ff4318ebb2718c9406399b4e4f5..d41987060c2261f1a345752ecc46af1ec23b83ea 100644 +--- a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java +@@ -95,9 +95,11 @@ public class GrindstoneMenu extends AbstractContainerMenu { + + @Override + public void onTake(net.minecraft.world.entity.player.Player player, ItemStack stack) { ++ ItemStack itemstack = activeQuickItem == null ? stack : activeQuickItem; // Purpur + context.execute((world, blockposition) -> { ++ org.purpurmc.purpur.event.inventory.GrindstoneTakeResultEvent grindstoneTakeResultEvent = new org.purpurmc.purpur.event.inventory.GrindstoneTakeResultEvent(player.getBukkitEntity(), getBukkitView(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), this.getExperienceAmount(world)); grindstoneTakeResultEvent.callEvent(); // Purpur + if (world instanceof ServerLevel) { +- ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), this.getExperienceAmount(world), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player); // Paper ++ ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), grindstoneTakeResultEvent.getExperienceAmount(), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player); // Paper // Purpur + } + + world.levelEvent(1042, blockposition, 0); +@@ -130,7 +132,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + Enchantment enchantment = (Enchantment) entry.getKey(); + Integer integer = (Integer) entry.getValue(); + +- if (!enchantment.isCurse()) { ++ if (!org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains(enchantment)) { // Purpur + j += enchantment.getMinCost(integer); + } + } +@@ -230,7 +232,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + Entry entry = (Entry) iterator.next(); + Enchantment enchantment = (Enchantment) entry.getKey(); + +- if (!enchantment.isCurse() || EnchantmentHelper.getItemEnchantmentLevel(enchantment, itemstack2) == 0) { ++ if (!org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains(enchantment) || EnchantmentHelper.getItemEnchantmentLevel(enchantment, itemstack2) == 0) { // Purpur + itemstack2.enchant(enchantment, (Integer) entry.getValue()); + } + } +@@ -251,7 +253,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + + itemstack1.setCount(amount); + Map map = (Map) EnchantmentHelper.getEnchantments(item).entrySet().stream().filter((entry) -> { +- return ((Enchantment) entry.getKey()).isCurse(); ++ return org.purpurmc.purpur.PurpurConfig.grindstoneIgnoredEnchants.contains((Enchantment) entry.getKey()); // Purpur + }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + EnchantmentHelper.setEnchantments(map, itemstack1); +@@ -267,6 +269,20 @@ public class GrindstoneMenu extends AbstractContainerMenu { + itemstack1.setRepairCost(AnvilMenu.calculateIncreasedRepairCost(itemstack1.getBaseRepairCost())); + } + ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.grindstoneRemoveAttributes && itemstack1.getTag() != null) { ++ for (String key : itemstack1.getTag().getAllKeys()) { ++ if (!key.equals("display")) { ++ itemstack1.getTag().remove(key); ++ } ++ } ++ } ++ ++ if (org.purpurmc.purpur.PurpurConfig.grindstoneRemoveDisplay && itemstack1.getTag() != null) { ++ itemstack1.getTag().remove("display"); ++ } ++ // Purpur end ++ + return itemstack1; + } + +@@ -328,7 +344,9 @@ public class GrindstoneMenu extends AbstractContainerMenu { + return ItemStack.EMPTY; + } + ++ this.activeQuickItem = itemstack; // Purpur + slot1.onTake(player, itemstack1); ++ this.activeQuickItem = null; // Purpur + } + + return itemstack; +diff --git a/src/main/java/net/minecraft/world/inventory/InventoryMenu.java b/src/main/java/net/minecraft/world/inventory/InventoryMenu.java +index da0f5c5e6ca7ce7b38792e6da52c5cdcdbae3b78..4136bcd49fe05d916ab65de0e866145185db4204 100644 +--- a/src/main/java/net/minecraft/world/inventory/InventoryMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/InventoryMenu.java +@@ -4,6 +4,7 @@ import com.mojang.datafixers.util.Pair; + import net.minecraft.network.chat.Component; + import net.minecraft.resources.ResourceLocation; + import net.minecraft.world.Container; ++import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.EquipmentSlot; + import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.player.Inventory; +@@ -95,7 +96,7 @@ public class InventoryMenu extends RecipeBookMenu { + public boolean mayPickup(Player playerEntity) { + ItemStack itemstack = this.getItem(); + +- return !itemstack.isEmpty() && !playerEntity.isCreative() && EnchantmentHelper.hasBindingCurse(itemstack) ? false : super.mayPickup(playerEntity); ++ return !itemstack.isEmpty() && !playerEntity.isCreative() && EnchantmentHelper.hasBindingCurse(itemstack) ? playerEntity.level.purpurConfig.playerRemoveBindingWithWeakness && playerEntity.hasEffect(MobEffects.WEAKNESS) : super.mayPickup(playerEntity); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java b/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java +index ff770b9ce68a62418de0c7ed389650626fa1dcb2..102739c0089ff3f6b3432f954304d43a3dfebc35 100644 +--- a/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/ItemCombinerMenu.java +@@ -177,7 +177,9 @@ public abstract class ItemCombinerMenu extends AbstractContainerMenu { + return ItemStack.EMPTY; + } + ++ this.activeQuickItem = itemstack; // Purpur + slot1.onTake(player, itemstack1); ++ this.activeQuickItem = null; // Purpur + } + + return itemstack; +diff --git a/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java b/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java +index 4703f23316f82a1a942907b46d2d6dcb7d70ec37..162798f57a05b78121fa6c4fadf5adee80fbe221 100644 +--- a/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java ++++ b/src/main/java/net/minecraft/world/inventory/PlayerEnderChestContainer.java +@@ -30,11 +30,18 @@ public class PlayerEnderChestContainer extends SimpleContainer { + } + + public PlayerEnderChestContainer(Player owner) { +- super(27); ++ super(org.purpurmc.purpur.PurpurConfig.enderChestSixRows ? 54 : 27); // Purpur + this.owner = owner; + // CraftBukkit end + } + ++ // Purpur start ++ @Override ++ public int getContainerSize() { ++ return owner.sixRowEnderchestSlotCount < 0 ? super.getContainerSize() : owner.sixRowEnderchestSlotCount; ++ } ++ // Purpur end ++ + public void setActiveChest(EnderChestBlockEntity blockEntity) { + this.activeChest = blockEntity; + } +diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java +index d7a0cbde8f8c99276307502674c71463fbe7e89c..3500c56cb85d8c76b2acd77976d374eaf487b3b3 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorItem.java +@@ -60,7 +60,7 @@ public class ArmorItem extends Item implements Equipable { + return false; + } else { + LivingEntity entityliving = (LivingEntity) list.get(0); +- EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(armor); ++ EquipmentSlot enumitemslot = pointer.getLevel().purpurConfig.dispenserApplyCursedArmor ? Mob.getEquipmentSlotForItem(armor) : Mob.getSlotForDispenser(armor); if (enumitemslot == null) return false; // Purpur + ItemStack itemstack1 = armor.copyWithCount(1); // Paper - shrink below and single item in event + // CraftBukkit start + Level world = pointer.getLevel(); +diff --git a/src/main/java/net/minecraft/world/item/ArmorStandItem.java b/src/main/java/net/minecraft/world/item/ArmorStandItem.java +index 7cffc64573008502bdd14ae4906fe51166b12fb3..1feafdbb48cf760cb6ebf95d5be2c32bdb1ad44f 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorStandItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorStandItem.java +@@ -58,6 +58,14 @@ public class ArmorStandItem extends Item { + return InteractionResult.FAIL; + } + // CraftBukkit end ++ // Purpur start ++ if (world.purpurConfig.persistentDroppableEntityDisplayNames && itemstack.hasCustomHoverName()) { ++ entityarmorstand.setCustomName(itemstack.getHoverName()); ++ if (world.purpurConfig.armorstandSetNameVisible) { ++ entityarmorstand.setCustomNameVisible(true); ++ } ++ } ++ // Purpur end + worldserver.addFreshEntityWithPassengers(entityarmorstand); + world.playSound((Player) null, entityarmorstand.getX(), entityarmorstand.getY(), entityarmorstand.getZ(), SoundEvents.ARMOR_STAND_PLACE, SoundSource.BLOCKS, 0.75F, 0.8F); + entityarmorstand.gameEvent(GameEvent.ENTITY_PLACE, context.getPlayer()); +diff --git a/src/main/java/net/minecraft/world/item/AxeItem.java b/src/main/java/net/minecraft/world/item/AxeItem.java +index 9c7d0b9cc2fa98d5785c914c0183f7d4b5b1c1ea..89a4ab17ca8d2aa1f52b041c610d7de19bf55e66 100644 +--- a/src/main/java/net/minecraft/world/item/AxeItem.java ++++ b/src/main/java/net/minecraft/world/item/AxeItem.java +@@ -33,29 +33,32 @@ public class AxeItem extends DiggerItem { + BlockPos blockPos = context.getClickedPos(); + Player player = context.getPlayer(); + BlockState blockState = level.getBlockState(blockPos); +- Optional optional = this.getStripped(blockState); +- Optional optional2 = WeatheringCopper.getPrevious(blockState); +- Optional optional3 = Optional.ofNullable(HoneycombItem.WAX_OFF_BY_BLOCK.get().get(blockState.getBlock())).map((block) -> { +- return block.withPropertiesOf(blockState); +- }); ++ // Purpur start ++ Block clickedBlock = level.getBlockState(blockPos).getBlock(); ++ Optional optional = Optional.ofNullable(level.purpurConfig.axeStrippables.get(blockState.getBlock())); ++ Optional optional2 = Optional.ofNullable(level.purpurConfig.axeWeatherables.get(blockState.getBlock())); ++ Optional optional3 = Optional.ofNullable(level.purpurConfig.axeWaxables.get(blockState.getBlock())); ++ // Purpur end + ItemStack itemStack = context.getItemInHand(); +- Optional optional4 = Optional.empty(); ++ Optional optional4 = Optional.empty(); // Purpur + if (optional.isPresent()) { +- level.playSound(player, blockPos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!STRIPPABLES.containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + optional4 = optional; + } else if (optional2.isPresent()) { +- level.playSound(player, blockPos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!HoneycombItem.WAXABLES.get().containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + level.levelEvent(player, 3005, blockPos, 0); + optional4 = optional2; + } else if (optional3.isPresent()) { +- level.playSound(player, blockPos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!HoneycombItem.WAX_OFF_BY_BLOCK.get().containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + level.levelEvent(player, 3004, blockPos, 0); + optional4 = optional3; + } + + if (optional4.isPresent()) { ++ org.purpurmc.purpur.tool.Actionable actionable = optional4.get(); ++ BlockState state = actionable.into().withPropertiesOf(blockState); // Purpur + // Paper start - EntityChangeBlockEvent +- if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, optional4.get()).isCancelled()) { ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, state).isCancelled()) { // Purpur + return InteractionResult.PASS; + } + // Paper end +@@ -63,15 +66,22 @@ public class AxeItem extends DiggerItem { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger((ServerPlayer)player, blockPos, itemStack); + } + +- level.setBlock(blockPos, optional4.get(), 11); +- level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, optional4.get())); ++ // Purpur start ++ level.setBlock(blockPos, state, 11); ++ actionable.drops().forEach((drop, chance) -> { ++ if (level.random.nextDouble() < chance) { ++ Block.popResourceFromFace(level, blockPos, context.getClickedFace(), new ItemStack(drop)); ++ } ++ }); ++ level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, state)); ++ // Purpur end + if (player != null) { + itemStack.hurtAndBreak(1, player, (p) -> { + p.broadcastBreakEvent(context.getHand()); + }); + } + +- return InteractionResult.sidedSuccess(level.isClientSide); ++ return InteractionResult.SUCCESS; // Purpur - force arm swing + } else { + return InteractionResult.PASS; + } +diff --git a/src/main/java/net/minecraft/world/item/BlockItem.java b/src/main/java/net/minecraft/world/item/BlockItem.java +index ebee8de2ed831755b6fd154f6cc77ac993839bb9..9060a844cd3bb3b62171872d84516b9195b9b677 100644 +--- a/src/main/java/net/minecraft/world/item/BlockItem.java ++++ b/src/main/java/net/minecraft/world/item/BlockItem.java +@@ -153,7 +153,24 @@ public class BlockItem extends Item { + } + + protected boolean updateCustomBlockEntityTag(BlockPos pos, Level world, @Nullable Player player, ItemStack stack, BlockState state) { +- return BlockItem.updateCustomBlockEntityTag(world, player, pos, stack); ++ // Purpur start ++ boolean handled = updateCustomBlockEntityTag(world, player, pos, stack); ++ if (world.purpurConfig.persistentTileEntityDisplayNames && stack.hasTag()) { ++ CompoundTag display = stack.getTagElement("display"); ++ if (display != null) { ++ BlockEntity blockEntity = world.getBlockEntity(pos); ++ if (blockEntity != null) { ++ if (display.contains("Name", 8)) { ++ blockEntity.setPersistentDisplayName(display.getString("Name")); ++ } ++ if (display.contains("Lore", 9)) { ++ blockEntity.setPersistentLore(display.getList("Lore", 8)); ++ } ++ } ++ } ++ } ++ return handled; ++ // Purpur end + } + + @Nullable +@@ -288,7 +305,7 @@ public class BlockItem extends Item { + + @Override + public void onDestroyed(ItemEntity entity) { +- if (this.block instanceof ShulkerBoxBlock) { ++ if (this.block instanceof ShulkerBoxBlock && entity.level.purpurConfig.shulkerBoxItemDropContentsWhenDestroyed) { + ItemStack itemstack = entity.getItem(); + CompoundTag nbttagcompound = BlockItem.getBlockEntityData(itemstack); + +diff --git a/src/main/java/net/minecraft/world/item/BoatItem.java b/src/main/java/net/minecraft/world/item/BoatItem.java +index 1a95ac11a2fbc811c89afa3adf38e0fc9eaab09b..91280f8c39ea191b90da2a9ff5c49f43c255bd9a 100644 +--- a/src/main/java/net/minecraft/world/item/BoatItem.java ++++ b/src/main/java/net/minecraft/world/item/BoatItem.java +@@ -69,6 +69,11 @@ public class BoatItem extends Item { + + entityboat.setVariant(this.type); + entityboat.setYRot(user.getYRot()); ++ // Purpur start ++ if (world.purpurConfig.persistentDroppableEntityDisplayNames && itemstack.hasCustomHoverName()) { ++ entityboat.setCustomName(itemstack.getHoverName()); ++ } ++ // Purpur end + if (!world.noCollision(entityboat, entityboat.getBoundingBox())) { + return InteractionResultHolder.fail(itemstack); + } else { +diff --git a/src/main/java/net/minecraft/world/item/BowItem.java b/src/main/java/net/minecraft/world/item/BowItem.java +index 08d597db1a5345a343777a01427655e6bf2c926b..d45a2f49c82d00801578c34e5f5277fc5e82be87 100644 +--- a/src/main/java/net/minecraft/world/item/BowItem.java ++++ b/src/main/java/net/minecraft/world/item/BowItem.java +@@ -38,13 +38,13 @@ public class BowItem extends ProjectileWeaponItem implements Vanishable { + float f = BowItem.getPowerForTime(j); + + if ((double) f >= 0.1D) { +- boolean flag1 = flag && itemstack1.is(Items.ARROW); ++ boolean flag1 = flag && ((itemstack1.is(Items.ARROW) && world.purpurConfig.infinityWorksWithNormalArrows) || (itemstack1.is(Items.TIPPED_ARROW) && world.purpurConfig.infinityWorksWithTippedArrows) || (itemstack1.is(Items.SPECTRAL_ARROW) && world.purpurConfig.infinityWorksWithSpectralArrows)); // Purpur if (!world.isClientSide) { + + if (!world.isClientSide) { + ArrowItem itemarrow = (ArrowItem) (itemstack1.getItem() instanceof ArrowItem ? itemstack1.getItem() : Items.ARROW); + AbstractArrow entityarrow = itemarrow.createArrow(world, itemstack1, entityhuman); + +- entityarrow.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, f * 3.0F, 1.0F); ++ entityarrow.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, f * 3.0F, (float) world.purpurConfig.bowProjectileOffset); // Purpur + if (f == 1.0F) { + entityarrow.setCritArrow(true); + } +@@ -64,6 +64,13 @@ public class BowItem extends ProjectileWeaponItem implements Vanishable { + if (EnchantmentHelper.getItemEnchantmentLevel(Enchantments.FLAMING_ARROWS, stack) > 0) { + entityarrow.setSecondsOnFire(100); + } ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.MOB_LOOTING, stack); ++ ++ if (lootingLevel > 0) { ++ entityarrow.setLootingLevel(lootingLevel); ++ } ++ // Purpur end + // CraftBukkit start + org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(entityhuman, stack, itemstack1, entityarrow, entityhuman.getUsedItemHand(), f, !flag1); + if (event.isCancelled()) { +@@ -132,7 +139,7 @@ public class BowItem extends ProjectileWeaponItem implements Vanishable { + ItemStack itemstack = user.getItemInHand(hand); + boolean flag = !user.getProjectile(itemstack).isEmpty(); + +- if (!user.getAbilities().instabuild && !flag) { ++ if (!(world.purpurConfig.infinityWorksWithoutArrows && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY_ARROWS, itemstack) > 0) && !user.getAbilities().instabuild && !flag) { // Purpur + return InteractionResultHolder.fail(itemstack); + } else { + user.startUsingItem(hand); +diff --git a/src/main/java/net/minecraft/world/item/BucketItem.java b/src/main/java/net/minecraft/world/item/BucketItem.java +index 5c6aa9c464784ad5ee366412d080c72d3d22a76f..c03abc9589bf5f37abc1b0d355ed9784bac31a93 100644 +--- a/src/main/java/net/minecraft/world/item/BucketItem.java ++++ b/src/main/java/net/minecraft/world/item/BucketItem.java +@@ -166,7 +166,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + // CraftBukkit end + if (!flag1) { + return movingobjectpositionblock != null && this.emptyContents(entityhuman, world, movingobjectpositionblock.getBlockPos().relative(movingobjectpositionblock.getDirection()), (BlockHitResult) null, enumdirection, clicked, itemstack, enumhand); // CraftBukkit +- } else if (world.dimensionType().ultraWarm() && this.content.is(FluidTags.WATER)) { ++ } else if ((world.dimensionType().ultraWarm() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) && this.content.is(FluidTags.WATER)) { // Purpur + int i = blockposition.getX(); + int j = blockposition.getY(); + int k = blockposition.getZ(); +@@ -174,7 +174,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + world.playSound(entityhuman, blockposition, SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.5F, 2.6F + (world.random.nextFloat() - world.random.nextFloat()) * 0.8F); + + for (int l = 0; l < 8; ++l) { +- world.addParticle(ParticleTypes.LARGE_SMOKE, (double) i + Math.random(), (double) j + Math.random(), (double) k + Math.random(), 0.0D, 0.0D, 0.0D); ++ ((ServerLevel) world).sendParticles(null, ParticleTypes.LARGE_SMOKE, (double) i + Math.random(), (double) j + Math.random(), (double) k + Math.random(), 1, 0.0D, 0.0D, 0.0D, 0.0D, true); // Purpur + } + + return true; +diff --git a/src/main/java/net/minecraft/world/item/CrossbowItem.java b/src/main/java/net/minecraft/world/item/CrossbowItem.java +index bc4f04c2512191da3c9e1c49f0716bb9128fc754..310e03d8cc07f95927d9806fc80a4215283d2ef5 100644 +--- a/src/main/java/net/minecraft/world/item/CrossbowItem.java ++++ b/src/main/java/net/minecraft/world/item/CrossbowItem.java +@@ -64,7 +64,7 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + ItemStack itemstack = user.getItemInHand(hand); + + if (CrossbowItem.isCharged(itemstack)) { +- CrossbowItem.performShooting(world, user, hand, itemstack, CrossbowItem.getShootingPower(itemstack), 1.0F); ++ CrossbowItem.performShooting(world, user, hand, itemstack, CrossbowItem.getShootingPower(itemstack), (float) world.purpurConfig.crossbowProjectileOffset); // Purpur + CrossbowItem.setCharged(itemstack, false); + return InteractionResultHolder.consume(itemstack); + } else if (!user.getProjectile(itemstack).isEmpty()) { +@@ -113,7 +113,7 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + // Paper end + int i = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.MULTISHOT, projectile); + int j = i == 0 ? 1 : 3; +- boolean flag = !consume || shooter instanceof Player && ((Player) shooter).getAbilities().instabuild; // Paper - add consume ++ boolean flag = !consume || shooter instanceof Player && ((Player) shooter).getAbilities().instabuild || (org.purpurmc.purpur.PurpurConfig.allowCrossbowInfinity && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY_ARROWS, projectile) > 0); // Paper - add consume // Purpur + ItemStack itemstack1 = shooter.getProjectile(projectile); + ItemStack itemstack2 = itemstack1.copy(); + +@@ -294,6 +294,14 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + entityarrow.setPierceLevel((byte) i); + } + ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.MOB_LOOTING, crossbow); ++ ++ if (lootingLevel > 0) { ++ entityarrow.setLootingLevel(lootingLevel); ++ } ++ // Purpur end ++ + return entityarrow; + } + +@@ -303,7 +311,7 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + + for (int i = 0; i < list.size(); ++i) { + ItemStack itemstack1 = (ItemStack) list.get(i); +- boolean flag = entity instanceof Player && ((Player) entity).getAbilities().instabuild; ++ boolean flag = entity instanceof Player && ((Player) entity).getAbilities().instabuild || (org.purpurmc.purpur.PurpurConfig.allowCrossbowInfinity && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY_ARROWS, stack) > 0); // Purpur + + if (!itemstack1.isEmpty()) { + if (i == 0) { +diff --git a/src/main/java/net/minecraft/world/item/DyeColor.java b/src/main/java/net/minecraft/world/item/DyeColor.java +index 2170715ed0e81a3055e4ab546c8b294c5ef7f142..beae4e2b9f61df83215de860d64c4ce2d3482004 100644 +--- a/src/main/java/net/minecraft/world/item/DyeColor.java ++++ b/src/main/java/net/minecraft/world/item/DyeColor.java +@@ -103,4 +103,10 @@ public enum DyeColor implements StringRepresentable { + public String getSerializedName() { + return this.name; + } ++ ++ // Purpur start ++ public static DyeColor random(net.minecraft.util.RandomSource random) { ++ return values()[random.nextInt(values().length)]; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/item/EggItem.java b/src/main/java/net/minecraft/world/item/EggItem.java +index 58cb992c5defec2f092755cbde661ff10f38bf9d..52f48681407d23f0925f4c9c072d5f0a2a6b1778 100644 +--- a/src/main/java/net/minecraft/world/item/EggItem.java ++++ b/src/main/java/net/minecraft/world/item/EggItem.java +@@ -24,7 +24,7 @@ public class EggItem extends Item { + ThrownEgg entityegg = new ThrownEgg(world, user); + + entityegg.setItem(itemstack); +- entityegg.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entityegg.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.eggProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entityegg.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entityegg)) { +diff --git a/src/main/java/net/minecraft/world/item/EnderpearlItem.java b/src/main/java/net/minecraft/world/item/EnderpearlItem.java +index 749ab72edc0d2e9c6f1161415ab8d59d3d6ca976..6b27d98d06b163243bb0e1bb979aad03f48d7770 100644 +--- a/src/main/java/net/minecraft/world/item/EnderpearlItem.java ++++ b/src/main/java/net/minecraft/world/item/EnderpearlItem.java +@@ -24,7 +24,7 @@ public class EnderpearlItem extends Item { + ThrownEnderpearl entityenderpearl = new ThrownEnderpearl(world, user); + + entityenderpearl.setItem(itemstack); +- entityenderpearl.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entityenderpearl.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.enderPearlProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entityenderpearl.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entityenderpearl)) { +@@ -36,7 +36,7 @@ public class EnderpearlItem extends Item { + + world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENDER_PEARL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (net.minecraft.world.entity.Entity.SHARED_RANDOM.nextFloat() * 0.4F + 0.8F)); + user.awardStat(Stats.ITEM_USED.get(this)); +- user.getCooldowns().addCooldown(this, 20); ++ user.getCooldowns().addCooldown(this, user.getAbilities().instabuild ? world.purpurConfig.enderPearlCooldownCreative : world.purpurConfig.enderPearlCooldown); // Purpur + } else { + // Paper end + if (user instanceof net.minecraft.server.level.ServerPlayer) { +diff --git a/src/main/java/net/minecraft/world/item/FireworkRocketItem.java b/src/main/java/net/minecraft/world/item/FireworkRocketItem.java +index 82b0bda3e35ec2157a477e1a17b2b46baadc97d9..0fc45b1048a1c4e0dc2bd1ae0437eecbe113cf96 100644 +--- a/src/main/java/net/minecraft/world/item/FireworkRocketItem.java ++++ b/src/main/java/net/minecraft/world/item/FireworkRocketItem.java +@@ -15,6 +15,7 @@ import net.minecraft.util.ByIdMap; + import net.minecraft.world.InteractionHand; + import net.minecraft.world.InteractionResult; + import net.minecraft.world.InteractionResultHolder; ++import net.minecraft.world.entity.EquipmentSlot; + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.entity.projectile.FireworkRocketEntity; + import net.minecraft.world.item.context.UseOnContext; +@@ -69,6 +70,14 @@ public class FireworkRocketItem extends Item { + com.destroystokyo.paper.event.player.PlayerElytraBoostEvent event = new com.destroystokyo.paper.event.player.PlayerElytraBoostEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Firework) fireworkRocketEntity.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(fireworkRocketEntity)) { + user.awardStat(Stats.ITEM_USED.get(this)); ++ // Purpur start ++ if (world.purpurConfig.elytraDamagePerFireworkBoost > 0) { ++ ItemStack chestItem = user.getItemBySlot(EquipmentSlot.CHEST); ++ if (chestItem.getItem() == Items.ELYTRA) { ++ chestItem.hurtAndBreak(world.purpurConfig.elytraDamagePerFireworkBoost, user, (entityliving) -> entityliving.broadcastBreakEvent(EquipmentSlot.CHEST)); ++ } ++ } ++ // Purpur end + if (event.shouldConsume() && !user.getAbilities().instabuild) { + itemStack.shrink(1); + } else ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory(); +diff --git a/src/main/java/net/minecraft/world/item/HangingEntityItem.java b/src/main/java/net/minecraft/world/item/HangingEntityItem.java +index b2ad6d230de2c29f371178bccde1111c7532ee70..6667926519a0f1c151e53f59cce36e7417dfc1cd 100644 +--- a/src/main/java/net/minecraft/world/item/HangingEntityItem.java ++++ b/src/main/java/net/minecraft/world/item/HangingEntityItem.java +@@ -48,7 +48,7 @@ public class HangingEntityItem extends Item { + return InteractionResult.FAIL; + } else { + Level world = context.getLevel(); +- Object object; ++ Entity object; // Purpur + + if (this.type == EntityType.PAINTING) { + Optional optional = Painting.create(world, blockposition1, enumdirection); +@@ -72,6 +72,11 @@ public class HangingEntityItem extends Item { + + if (nbttagcompound != null) { + EntityType.updateCustomEntityTag(world, entityhuman, (Entity) object, nbttagcompound); ++ // Purpur start ++ if (world.purpurConfig.persistentDroppableEntityDisplayNames && itemstack.hasCustomHoverName()) { ++ object.setCustomName(itemstack.getHoverName()); ++ } ++ // Purpur end + } + + if (((HangingEntity) object).survives()) { +diff --git a/src/main/java/net/minecraft/world/item/HoeItem.java b/src/main/java/net/minecraft/world/item/HoeItem.java +index 180aec596110309aade13d2080f8824d152b07cb..c4aec1e5135a79837918b692e75a7b55d5cffeb0 100644 +--- a/src/main/java/net/minecraft/world/item/HoeItem.java ++++ b/src/main/java/net/minecraft/world/item/HoeItem.java +@@ -34,15 +34,23 @@ public class HoeItem extends DiggerItem { + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + BlockPos blockPos = context.getClickedPos(); +- Pair, Consumer> pair = TILLABLES.get(level.getBlockState(blockPos).getBlock()); +- if (pair == null) { +- return InteractionResult.PASS; +- } else { +- Predicate predicate = pair.getFirst(); +- Consumer consumer = pair.getSecond(); ++ // Purpur start ++ Block clickedBlock = level.getBlockState(blockPos).getBlock(); ++ var tillable = level.purpurConfig.hoeTillables.get(level.getBlockState(blockPos).getBlock()); ++ if (tillable == null) { return InteractionResult.PASS; } else { ++ Predicate predicate = tillable.condition().predicate(); ++ Consumer consumer = (ctx) -> { ++ level.setBlock(blockPos, tillable.into().defaultBlockState(), 11); ++ tillable.drops().forEach((drop, chance) -> { ++ if (level.random.nextDouble() < chance) { ++ Block.popResourceFromFace(level, blockPos, ctx.getClickedFace(), new ItemStack(drop)); ++ } ++ }); ++ }; ++ // Purpur end + if (predicate.test(context)) { + Player player = context.getPlayer(); +- level.playSound(player, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); ++ if (!TILLABLES.containsKey(clickedBlock)) level.playSound(null, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound + if (!level.isClientSide) { + consumer.accept(context); + if (player != null) { +@@ -52,7 +60,7 @@ public class HoeItem extends DiggerItem { + } + } + +- return InteractionResult.sidedSuccess(level.isClientSide); ++ return InteractionResult.SUCCESS; // Purpur - force arm swing + } else { + return InteractionResult.PASS; + } +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index d81fcbadc5c0b3d4b54dde5d47a0f847d8ec6918..aac8c78cec1c935d24207093a36b70a1ed082703 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -111,6 +111,7 @@ import org.bukkit.event.world.StructureGrowEvent; + + public final class ItemStack { + ++ public boolean isExactRecipeIngredient = false; // PaperPR + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> { + return instance.group(BuiltInRegistries.ITEM.byNameCodec().fieldOf("id").forGetter((itemstack) -> { + return itemstack.item; +@@ -417,6 +418,7 @@ public final class ItemStack { + world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 + for (BlockState blockstate : blocks) { + blockstate.update(true, false); ++ ((CraftBlock) blockstate.getBlock()).getNMS().getBlock().forgetPlacer(); // Purpur + } + world.preventPoiUpdated = false; + +@@ -446,6 +448,7 @@ public final class ItemStack { + if (!(block.getBlock() instanceof BaseEntityBlock)) { // Containers get placed automatically + block.getBlock().onPlace(block, world, newblockposition, oldBlock, true, itemactioncontext); // Paper - pass itemactioncontext + } ++ block.getBlock().forgetPlacer(); // Purpur + + world.notifyAndUpdatePhysics(newblockposition, null, oldBlock, block, world.getBlockState(newblockposition), updateFlag, 512); // send null chunk as chunk.k() returns false by this point + } +@@ -566,6 +569,16 @@ public final class ItemStack { + return this.isDamageableItem() && this.getDamageValue() > 0; + } + ++ // Purpur start ++ public float getDamagePercent() { ++ if (isDamaged()) { ++ return (float) getDamageValue() / (float) getItem().getMaxDamage(); ++ } else { ++ return 0F; ++ } ++ } ++ // Purpur end ++ + public int getDamageValue() { + return this.tag == null ? 0 : this.tag.getInt("Damage"); + } +@@ -585,7 +598,7 @@ public final class ItemStack { + int j; + + if (amount > 0) { +- j = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, this); ++ j = (getItem() == Items.ELYTRA && player != null && player.level.purpurConfig.elytraIgnoreUnbreaking) ? 0 : EnchantmentHelper.getItemEnchantmentLevel(Enchantments.UNBREAKING, this); + int k = 0; + + for (int l = 0; j > 0 && l < amount; ++l) { +@@ -640,6 +653,12 @@ public final class ItemStack { + if (this.hurt(amount, entity.getRandom(), entity /*instanceof ServerPlayer ? (ServerPlayer) entity : null*/)) { // Paper - pass LivingEntity for EntityItemDamageEvent + breakCallback.accept(entity); + Item item = this.getItem(); ++ // Purpur start ++ if (item == Items.ELYTRA) { ++ setDamageValue(item.getMaxDamage() - 1); ++ return; ++ } ++ // Purpur end + // CraftBukkit start - Check for item breaking + if (this.count == 1 && entity instanceof net.minecraft.world.entity.player.Player) { + org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemBreakEvent((net.minecraft.world.entity.player.Player) entity, this); +@@ -1172,7 +1191,7 @@ public final class ItemStack { + + ListTag nbttaglist = this.tag.getList("Enchantments", 10); + +- nbttaglist.add(EnchantmentHelper.storeEnchantment(EnchantmentHelper.getEnchantmentId(enchantment), (byte) level)); ++ nbttaglist.add(EnchantmentHelper.storeEnchantment(EnchantmentHelper.getEnchantmentId(enchantment), (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels) ? (byte) level : (short) level)); // Purpur + processEnchantOrder(this.tag); // Paper + } + +@@ -1180,6 +1199,12 @@ public final class ItemStack { + return this.tag != null && this.tag.contains("Enchantments", 9) ? !this.tag.getList("Enchantments", 10).isEmpty() : false; + } + ++ // Purpur start ++ public boolean hasEnchantment(Enchantment enchantment) { ++ return isEnchanted() && EnchantmentHelper.deserializeEnchantments(getEnchantmentTags()).containsKey(enchantment); ++ } ++ // Purpur end ++ + public void addTagElement(String key, Tag element) { + this.getOrCreateTag().put(key, element); + } +diff --git a/src/main/java/net/minecraft/world/item/Items.java b/src/main/java/net/minecraft/world/item/Items.java +index 775823daa5187804d27e5ee696cd75f703bb067c..bc0915913d3d8fbe145ee7e19133c7de922e0c80 100644 +--- a/src/main/java/net/minecraft/world/item/Items.java ++++ b/src/main/java/net/minecraft/world/item/Items.java +@@ -292,7 +292,7 @@ public class Items { + public static final Item PURPUR_BLOCK = registerBlock(Blocks.PURPUR_BLOCK); + public static final Item PURPUR_PILLAR = registerBlock(Blocks.PURPUR_PILLAR); + public static final Item PURPUR_STAIRS = registerBlock(Blocks.PURPUR_STAIRS); +- public static final Item SPAWNER = registerBlock(Blocks.SPAWNER); ++ public static final Item SPAWNER = registerBlock(new org.purpurmc.purpur.item.SpawnerItem(Blocks.SPAWNER, new Item.Properties().rarity(Rarity.EPIC))); // Purpur + public static final Item CHEST = registerBlock(Blocks.CHEST); + public static final Item CRAFTING_TABLE = registerBlock(Blocks.CRAFTING_TABLE); + public static final Item FARMLAND = registerBlock(Blocks.FARMLAND); +@@ -1178,7 +1178,7 @@ public class Items { + public static final Item LANTERN = registerBlock(Blocks.LANTERN); + public static final Item SOUL_LANTERN = registerBlock(Blocks.SOUL_LANTERN); + public static final Item SWEET_BERRIES = registerItem("sweet_berries", new ItemNameBlockItem(Blocks.SWEET_BERRY_BUSH, (new Item.Properties()).food(Foods.SWEET_BERRIES))); +- public static final Item GLOW_BERRIES = registerItem("glow_berries", new ItemNameBlockItem(Blocks.CAVE_VINES, (new Item.Properties()).food(Foods.GLOW_BERRIES))); ++ public static final Item GLOW_BERRIES = registerItem("glow_berries", new org.purpurmc.purpur.item.GlowBerryItem(Blocks.CAVE_VINES, (new Item.Properties()).food(Foods.GLOW_BERRIES))); // Purpur + public static final Item CAMPFIRE = registerBlock(Blocks.CAMPFIRE); + public static final Item SOUL_CAMPFIRE = registerBlock(Blocks.SOUL_CAMPFIRE); + public static final Item SHROOMLIGHT = registerBlock(Blocks.SHROOMLIGHT); +@@ -1278,6 +1278,13 @@ public class Items { + ((BlockItem)item).registerBlocks(Item.BY_BLOCK, item); + } + ++ // Purpur start ++ if (item.getFoodProperties() != null) { ++ Foods.ALL_PROPERTIES.put(id.getPath(), item.getFoodProperties()); ++ Foods.DEFAULT_PROPERTIES.put(id.getPath(), item.getFoodProperties().copy()); ++ } ++ // Purpur end ++ + return Registry.register(BuiltInRegistries.ITEM, id, item); + } + } +diff --git a/src/main/java/net/minecraft/world/item/MilkBucketItem.java b/src/main/java/net/minecraft/world/item/MilkBucketItem.java +index f33977d95b6db473be4f95075ba99caf90ad0220..56dc04d8875971ee9a5d077a695509af74fe2473 100644 +--- a/src/main/java/net/minecraft/world/item/MilkBucketItem.java ++++ b/src/main/java/net/minecraft/world/item/MilkBucketItem.java +@@ -5,6 +5,8 @@ import net.minecraft.server.level.ServerPlayer; + import net.minecraft.stats.Stats; + import net.minecraft.world.InteractionHand; + import net.minecraft.world.InteractionResultHolder; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.LivingEntity; + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.level.Level; +@@ -31,7 +33,9 @@ public class MilkBucketItem extends Item { + } + + if (!world.isClientSide) { ++ MobEffectInstance badOmen = user.getEffect(MobEffects.BAD_OMEN); + user.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.MILK); // CraftBukkit ++ if (!world.purpurConfig.milkCuresBadOmen && badOmen != null) user.addEffect(badOmen); // Purpur + } + + return stack.isEmpty() ? new ItemStack(Items.BUCKET) : stack; +diff --git a/src/main/java/net/minecraft/world/item/MinecartItem.java b/src/main/java/net/minecraft/world/item/MinecartItem.java +index c6d2f764efa9b8bec730bbe757d480e365b25ccc..33a30d26da2401535f0a72acb2bbffec1aef151e 100644 +--- a/src/main/java/net/minecraft/world/item/MinecartItem.java ++++ b/src/main/java/net/minecraft/world/item/MinecartItem.java +@@ -120,8 +120,9 @@ public class MinecartItem extends Item { + BlockState iblockdata = world.getBlockState(blockposition); + + if (!iblockdata.is(BlockTags.RAILS)) { +- return InteractionResult.FAIL; +- } else { ++ if (!world.purpurConfig.minecartPlaceAnywhere) return InteractionResult.FAIL; ++ if (iblockdata.getMaterial().isSolid()) blockposition = blockposition.relative(context.getClickedFace()); ++ } // else { // Purpur - place minecarts anywhere + ItemStack itemstack = context.getItemInHand(); + + if (!world.isClientSide) { +@@ -149,6 +150,6 @@ public class MinecartItem extends Item { + + itemstack.shrink(1); + return InteractionResult.sidedSuccess(world.isClientSide); +- } ++ // } // Purpur - place minecarts anywhere + } + } +diff --git a/src/main/java/net/minecraft/world/item/NameTagItem.java b/src/main/java/net/minecraft/world/item/NameTagItem.java +index 623f78c078fb3aa2665d7e8a37672438227bce6b..500c69e555c7247e20ef8cc59d83415578f44427 100644 +--- a/src/main/java/net/minecraft/world/item/NameTagItem.java ++++ b/src/main/java/net/minecraft/world/item/NameTagItem.java +@@ -24,6 +24,7 @@ public class NameTagItem extends Item { + if (!event.callEvent()) return InteractionResult.PASS; + LivingEntity newEntityLiving = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getEntity()).getHandle(); + newEntityLiving.setCustomName(event.getName() != null ? PaperAdventure.asVanilla(event.getName()) : null); ++ if (user.level.purpurConfig.armorstandFixNametags && entity instanceof net.minecraft.world.entity.decoration.ArmorStand) entity.setCustomNameVisible(true); // Purpur + if (event.isPersistent() && newEntityLiving instanceof Mob) { + ((Mob) newEntityLiving).setPersistenceRequired(); + // Paper end +diff --git a/src/main/java/net/minecraft/world/item/ShovelItem.java b/src/main/java/net/minecraft/world/item/ShovelItem.java +index c7195f2e12bbd6545f7bffcc2b4ba5cc3d48df20..5e730bc9c8ff94b16ac2bf8567dda8aea2ee4b2a 100644 +--- a/src/main/java/net/minecraft/world/item/ShovelItem.java ++++ b/src/main/java/net/minecraft/world/item/ShovelItem.java +@@ -34,7 +34,7 @@ public class ShovelItem extends DiggerItem { + return InteractionResult.PASS; + } else { + Player player = context.getPlayer(); +- BlockState blockState2 = FLATTENABLES.get(blockState.getBlock()); ++ BlockState blockState2 = level.purpurConfig.shovelTurnsBlockToGrassPath.contains(blockState.getBlock()) ? Blocks.DIRT_PATH.defaultBlockState() : null; // Purpur + BlockState blockState3 = null; + Runnable afterAction = null; // Paper + if (blockState2 != null && level.getBlockState(blockPos.above()).isAir()) { +diff --git a/src/main/java/net/minecraft/world/item/SnowballItem.java b/src/main/java/net/minecraft/world/item/SnowballItem.java +index ef3f90a5bcdd7b9815a4053cff166f9d2552f55d..e7e5e1cc92f56e3daba8fa09c59188febec5e8f2 100644 +--- a/src/main/java/net/minecraft/world/item/SnowballItem.java ++++ b/src/main/java/net/minecraft/world/item/SnowballItem.java +@@ -25,7 +25,7 @@ public class SnowballItem extends Item { + Snowball entitysnowball = new Snowball(world, user); + + entitysnowball.setItem(itemstack); +- entitysnowball.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, 1.0F); ++ entitysnowball.shootFromRotation(user, user.getXRot(), user.getYRot(), 0.0F, 1.5F, (float) world.purpurConfig.snowballProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) entitysnowball.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(entitysnowball)) { +diff --git a/src/main/java/net/minecraft/world/item/SpawnEggItem.java b/src/main/java/net/minecraft/world/item/SpawnEggItem.java +index 31268e25056f980798ef7db72c4f955a074cc639..955822da142a2463af536dd1ce48037deda41402 100644 +--- a/src/main/java/net/minecraft/world/item/SpawnEggItem.java ++++ b/src/main/java/net/minecraft/world/item/SpawnEggItem.java +@@ -68,6 +68,15 @@ public class SpawnEggItem extends Item { + SpawnerBlockEntity tileentitymobspawner = (SpawnerBlockEntity) tileentity; + EntityType entitytypes = this.getType(itemstack.getTag()); + ++ // Purpur start ++ org.bukkit.block.Block bukkitBlock = world.getWorld().getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()); ++ org.purpurmc.purpur.event.PlayerSetSpawnerTypeWithEggEvent event = new org.purpurmc.purpur.event.PlayerSetSpawnerTypeWithEggEvent((org.bukkit.entity.Player) context.getPlayer().getBukkitEntity(), bukkitBlock, (org.bukkit.block.CreatureSpawner) bukkitBlock.getState(), org.bukkit.entity.EntityType.fromName(entitytypes.getName())); ++ if (!event.callEvent()) { ++ return InteractionResult.FAIL; ++ } ++ entitytypes = EntityType.getFromBukkitType(event.getEntityType()); ++ // Purpur end ++ + tileentitymobspawner.setEntityId(entitytypes, world.getRandom()); + tileentity.setChanged(); + world.sendBlockUpdated(blockposition, iblockdata, iblockdata, 3); +diff --git a/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java b/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java +index de5bdceb4c8578fb972a2fd5ee0dfdae509e46dc..bcf63ccb6e679cb97d658780b2663aafa3568bcb 100644 +--- a/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java ++++ b/src/main/java/net/minecraft/world/item/ThrowablePotionItem.java +@@ -18,7 +18,7 @@ public class ThrowablePotionItem extends PotionItem { + if (!world.isClientSide) { + ThrownPotion thrownPotion = new ThrownPotion(world, user); + thrownPotion.setItem(itemStack); +- thrownPotion.shootFromRotation(user, user.getXRot(), user.getYRot(), -20.0F, 0.5F, 1.0F); ++ thrownPotion.shootFromRotation(user, user.getXRot(), user.getYRot(), -20.0F, 0.5F, (float) world.purpurConfig.throwablePotionProjectileOffset); // Purpur + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Projectile) thrownPotion.getBukkitEntity()); + if (event.callEvent() && world.addFreshEntity(thrownPotion)) { +diff --git a/src/main/java/net/minecraft/world/item/TridentItem.java b/src/main/java/net/minecraft/world/item/TridentItem.java +index 9365f886a23a71c41091b22d46896ff18a5a0635..d35432087c70ce66b74d1e27df19f462f22b1aa1 100644 +--- a/src/main/java/net/minecraft/world/item/TridentItem.java ++++ b/src/main/java/net/minecraft/world/item/TridentItem.java +@@ -77,11 +77,19 @@ public class TridentItem extends Item implements Vanishable { + if (k == 0) { + ThrownTrident entitythrowntrident = new ThrownTrident(world, entityhuman, stack); + +- entitythrowntrident.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, 2.5F + (float) k * 0.5F, 1.0F); ++ entitythrowntrident.shootFromRotation(entityhuman, entityhuman.getXRot(), entityhuman.getYRot(), 0.0F, 2.5F + (float) k * 0.5F, (float) world.purpurConfig.tridentProjectileOffset); // Purpur + if (entityhuman.getAbilities().instabuild) { + entitythrowntrident.pickup = AbstractArrow.Pickup.CREATIVE_ONLY; + } + ++ // Purpur start ++ int lootingLevel = EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.MOB_LOOTING, stack); ++ ++ if (lootingLevel > 0) { ++ entitythrowntrident.setLootingLevel(lootingLevel); ++ } ++ // Purpur end ++ + // CraftBukkit start + // Paper start + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) entityhuman.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), (org.bukkit.entity.Projectile) entitythrowntrident.getBukkitEntity()); +@@ -130,6 +138,14 @@ public class TridentItem extends Item implements Vanishable { + f2 *= f6 / f5; + f3 *= f6 / f5; + f4 *= f6 / f5; ++ ++ // Purpur start ++ ItemStack chestItem = entityhuman.getItemBySlot(EquipmentSlot.CHEST); ++ if (chestItem.getItem() == Items.ELYTRA && world.purpurConfig.elytraDamagePerTridentBoost > 0) { ++ chestItem.hurtAndBreak(world.purpurConfig.elytraDamagePerTridentBoost, entityhuman, (entity) -> entity.broadcastBreakEvent(EquipmentSlot.CHEST)); ++ } ++ // Purpur end ++ + entityhuman.push((double) f2, (double) f3, (double) f4); + entityhuman.startAutoSpinAttack(20); + if (entityhuman.isOnGround()) { +diff --git a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java +index 8d4aca59bd7518179520f4d4fb7137778e232d90..e24034d1ce4bb529de084aab69a531227e0c2f79 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java ++++ b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java +@@ -39,6 +39,7 @@ public final class Ingredient implements Predicate { + @Nullable + private IntList stackingIds; + public boolean exact; // CraftBukkit ++ public Predicate predicate; + + public Ingredient(Stream entries) { + this.values = (Ingredient.Value[]) entries.toArray((i) -> { +@@ -50,7 +51,11 @@ public final class Ingredient implements Predicate { + if (this.itemStacks == null) { + this.itemStacks = (ItemStack[]) Arrays.stream(this.values).flatMap((recipeitemstack_provider) -> { + return recipeitemstack_provider.getItems().stream(); +- }).distinct().toArray((i) -> { ++ // PaperPR start ++ }).distinct().peek(stack -> { ++ stack.isExactRecipeIngredient = this.exact; ++ }).toArray((i) -> { ++ // PaperPR end + return new ItemStack[i]; + }); + } +@@ -64,6 +69,12 @@ public final class Ingredient implements Predicate { + } else if (this.isEmpty()) { + return itemstack.isEmpty(); + } else { ++ // Purpur start ++ if (predicate != null) { ++ return predicate.test(itemstack.asBukkitCopy()); ++ } ++ // Purpur end ++ + ItemStack[] aitemstack = this.getItems(); + int i = aitemstack.length; + +@@ -99,7 +110,13 @@ public final class Ingredient implements Predicate { + for (int j = 0; j < i; ++j) { + ItemStack itemstack = aitemstack1[j]; + ++ // PaperPR start ++ if (itemstack.isExactRecipeIngredient) { ++ this.stackingIds.add(StackedContents.getExactStackingIndex(itemstack)); ++ } else { ++ // PaperPR end + this.stackingIds.add(StackedContents.getStackingIndex(itemstack)); ++ } // PaperPR + } + + this.stackingIds.sort(IntComparators.NATURAL_COMPARATOR); +diff --git a/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java b/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java +index 518d85a13c37a2f7d32ca0718323181048559986..27512787b37381a5236b1b473e9ce3f06df8e2d0 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/ArrowInfiniteEnchantment.java +@@ -7,6 +7,14 @@ public class ArrowInfiniteEnchantment extends Enchantment { + super(weight, EnchantmentCategory.BOW, slotTypes); + } + ++ // Purpur start ++ @Override ++ public boolean canEnchant(net.minecraft.world.item.ItemStack stack) { ++ // we have to cheat the system because this class is loaded before purpur's config is loaded ++ return (org.purpurmc.purpur.PurpurConfig.allowCrossbowInfinity ? EnchantmentCategory.BOW_AND_CROSSBOW : EnchantmentCategory.BOW).canEnchant(stack.getItem()); ++ } ++ // Purpur end ++ + @Override + public int getMinCost(int level) { + return 20; +@@ -19,6 +27,6 @@ public class ArrowInfiniteEnchantment extends Enchantment { + + @Override + public boolean checkCompatibility(Enchantment other) { +- return other instanceof MendingEnchantment ? false : super.checkCompatibility(other); ++ return other instanceof MendingEnchantment ? org.purpurmc.purpur.PurpurConfig.allowInfinityMending : super.checkCompatibility(other); + } + } +diff --git a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java +index 246516e67db0b8b197b287c067d5a0163d8bde22..fc2c35f57436371cb0111aedfd289ac95d506d07 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentCategory.java +@@ -121,6 +121,20 @@ public enum EnchantmentCategory { + public boolean canEnchant(Item item) { + return item instanceof Vanishable || Block.byItem(item) instanceof Vanishable || BREAKABLE.canEnchant(item); + } ++ // Purpur start ++ }, ++ BOW_AND_CROSSBOW { ++ @Override ++ public boolean canEnchant(Item item) { ++ return item instanceof BowItem || item instanceof CrossbowItem; ++ } ++ }, ++ WEAPON_AND_SHEARS { ++ @Override ++ public boolean canEnchant(Item item) { ++ return WEAPON.canEnchant(item) || item instanceof net.minecraft.world.item.ShearsItem; ++ } ++ // Purpur end + }; + + public abstract boolean canEnchant(Item item); +diff --git a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java +index ecf640b00007a386290f8dfe9935a8aa610079fd..1eec84e217f6dc929091fa7451cd235ef3623822 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/EnchantmentHelper.java +@@ -46,7 +46,7 @@ public class EnchantmentHelper { + } + + public static int getEnchantmentLevel(CompoundTag nbt) { +- return Mth.clamp(nbt.getInt("lvl"), 0, 255); ++ return Mth.clamp(nbt.getInt("lvl"), 0, (org.purpurmc.purpur.PurpurConfig.clampEnchantLevels) ? 255 : 32767); // Purpur + } + + @Nullable +@@ -278,6 +278,29 @@ public class EnchantmentHelper { + return getItemEnchantmentLevel(Enchantments.CHANNELING, stack) > 0; + } + ++ // Purpur start ++ @Nullable ++ public static Map.Entry getMostDamagedEquipment(Enchantment enchantment, LivingEntity entity) { ++ Map map = enchantment.getSlotItems(entity); ++ if (map.isEmpty()) { ++ return null; ++ } ++ Map.Entry item = null; ++ float maxPercent = 0F; ++ for (Map.Entry entry : map.entrySet()) { ++ ItemStack itemstack = entry.getValue(); ++ if (!itemstack.isEmpty() && itemstack.isDamaged() && getItemEnchantmentLevel(enchantment, itemstack) > 0) { ++ float percent = itemstack.getDamagePercent(); ++ if (item == null || percent > maxPercent) { ++ item = entry; ++ maxPercent = percent; ++ } ++ } ++ } ++ return item; ++ } ++ // Purpur end ++ + @Nullable + public static Map.Entry getRandomItemWith(Enchantment enchantment, LivingEntity entity) { + return getRandomItemWith(enchantment, entity, (stack) -> { +diff --git a/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java b/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java +index 4007c16550683e23b396dfdff29530a82523fe05..8fe09c13643d99639fb242da4367c42ef31b38b4 100644 +--- a/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java ++++ b/src/main/java/net/minecraft/world/item/enchantment/LootBonusEnchantment.java +@@ -7,6 +7,14 @@ public class LootBonusEnchantment extends Enchantment { + super(weight, target, slotTypes); + } + ++ // Purpur start ++ @Override ++ public boolean canEnchant(net.minecraft.world.item.ItemStack stack) { ++ // we have to cheat the system because this class is loaded before purpur's config is loaded ++ return (org.purpurmc.purpur.PurpurConfig.allowShearsLooting && this.category == EnchantmentCategory.WEAPON ? EnchantmentCategory.WEAPON_AND_SHEARS : this.category).canEnchant(stack.getItem()); ++ } ++ // Purpur end ++ + @Override + public int getMinCost(int level) { + return 15 + (level - 1) * 9; +diff --git a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java +index fd50d1c2435b82215bc5b3fdbe5044d426bc342e..68ffea572045634f1ad67a6954d480e6ae7833f5 100644 +--- a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java ++++ b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java +@@ -132,7 +132,12 @@ public class MerchantOffer { + } + + public void updateDemand() { +- this.demand = Math.max(0, this.demand + this.uses - (this.maxUses - this.uses)); // Paper ++ // Purpur start ++ this.updateDemand(0); ++ } ++ public void updateDemand(int minimumDemand) { ++ this.demand = Math.max(minimumDemand, this.demand + this.uses - (this.maxUses - this.uses)); ++ // Purpur end + } + + public ItemStack assemble() { +diff --git a/src/main/java/net/minecraft/world/level/BaseSpawner.java b/src/main/java/net/minecraft/world/level/BaseSpawner.java +index 31ac0e5ca26c7bdfa9b710d0bb78d846ddf6863e..feb65fc9ee04141fe6f77400660442ed207547a1 100644 +--- a/src/main/java/net/minecraft/world/level/BaseSpawner.java ++++ b/src/main/java/net/minecraft/world/level/BaseSpawner.java +@@ -55,6 +55,7 @@ public abstract class BaseSpawner { + } + + public boolean isNearPlayer(Level world, BlockPos pos) { ++ if (world.purpurConfig.spawnerDeactivateByRedstone && world.hasNeighborSignal(pos)) return false; // Purpur + return world.hasNearbyAlivePlayerThatAffectsSpawning((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange); // Paper - Affects Spawning API + } + +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 3b959f42d958bf0f426853aee56753d6c455fcdb..d17abb283ea818244df0379d6b57fc634071e0b9 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -154,7 +154,7 @@ public interface EntityGetter { + + default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { + for(Player player : this.players()) { +- if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { ++ if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player) && EntitySelector.notAfk.test(player)) { + double d = player.distanceToSqr(x, y, z); + if (range < 0.0D || d < range * range) { + return true; +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 185f7b1d4df59f5db7b85b529a2de6402630bf35..7a3d9c1f8abdbc25e88ca35da1546725f0013c68 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -86,7 +86,7 @@ public class Explosion { + this.hitPlayers = Maps.newHashMap(); + this.level = world; + this.source = entity; +- this.radius = (float) Math.max(power, 0.0); // CraftBukkit - clamp bad values ++ this.radius = (float) (world == null || world.purpurConfig.explosionClampRadius ? Math.max(power, 0.0) : power); // CraftBukkit - clamp bad values // Purpur + this.x = x; + this.y = y; + this.z = z; +@@ -137,10 +137,27 @@ public class Explosion { + + public void explode() { + // CraftBukkit start +- if (this.radius < 0.1F) { ++ if ((this.level == null || this.level.purpurConfig.explosionClampRadius) && this.radius < 0.1F) { // Purpur + return; + } + // CraftBukkit end ++ ++ // Purpur start - add PreExplodeEvents ++ if(this.source != null){ ++ Location location = new Location(this.level.getWorld(), this.x, this.y, this.z); ++ if(!new org.purpurmc.purpur.event.entity.PreEntityExplodeEvent(this.source.getBukkitEntity(), location, this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F).callEvent()) { ++ this.wasCanceled = true; ++ return; ++ } ++ }else { ++ Location location = new Location(this.level.getWorld(), this.x, this.y, this.z); ++ if(!new org.purpurmc.purpur.event.PreBlockExplodeEvent(location.getBlock(), this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F).callEvent()) { ++ this.wasCanceled = true; ++ return; ++ } ++ } ++ //Purpur end ++ + this.level.gameEvent(this.source, GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z)); + Set set = Sets.newHashSet(); + boolean flag = true; +@@ -382,7 +399,7 @@ public class Explosion { + if (!iblockdata.isAir() && iblockdata.isDestroyable()) { // Paper + BlockPos blockposition1 = blockposition.immutable(); + +- this.level.getProfiler().push("explosion_blocks"); ++ //this.level.getProfiler().push("explosion_blocks"); // Purpur + if (block.dropFromExplosion(this)) { + Level world = this.level; + +@@ -404,7 +421,7 @@ public class Explosion { + + this.level.setBlock(blockposition, Blocks.AIR.defaultBlockState(), 3); + block.wasExploded(this.level, blockposition, this); +- this.level.getProfiler().pop(); ++ //this.level.getProfiler().pop(); // Purpur + } + } + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 6aec1983a0236d6aa0507a2b3ad1c08b3330f0fc..b8001bca2a33ec1e60566948a651400418a6e9e7 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -177,6 +177,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Paper end + + public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray ++ public final org.purpurmc.purpur.PurpurWorldConfig purpurConfig; // Purpur + public final co.aikar.timings.WorldTimingsHandler timings; // Paper + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; +@@ -194,6 +195,49 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end - fix and optimise world upgrading + ++ // Purpur start ++ private com.google.common.cache.Cache playerBreedingCooldowns; ++ ++ private com.google.common.cache.Cache getNewBreedingCooldownCache() { ++ return com.google.common.cache.CacheBuilder.newBuilder().expireAfterWrite(this.purpurConfig.animalBreedingCooldownSeconds, java.util.concurrent.TimeUnit.SECONDS).build(); ++ } ++ ++ public void resetBreedingCooldowns() { ++ this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); ++ } ++ ++ public boolean hasBreedingCooldown(java.util.UUID player, Class animalType) { // Purpur ++ return this.playerBreedingCooldowns.getIfPresent(new BreedingCooldownPair(player, animalType)) != null; ++ } ++ ++ public void addBreedingCooldown(java.util.UUID player, Class animalType) { ++ this.playerBreedingCooldowns.put(new BreedingCooldownPair(player, animalType), new Object()); ++ } ++ ++ private static final class BreedingCooldownPair { ++ private final java.util.UUID playerUUID; ++ private final Class animalType; ++ ++ public BreedingCooldownPair(java.util.UUID playerUUID, Class animalType) { ++ this.playerUUID = playerUUID; ++ this.animalType = animalType; ++ } ++ ++ @Override ++ public boolean equals(Object o) { ++ if (this == o) return true; ++ if (o == null || getClass() != o.getClass()) return false; ++ BreedingCooldownPair that = (BreedingCooldownPair) o; ++ return playerUUID.equals(that.playerUUID) && animalType.equals(that.animalType); ++ } ++ ++ @Override ++ public int hashCode() { ++ return java.util.Objects.hash(playerUUID, animalType); ++ } ++ } ++ // Purpur end ++ + public CraftWorld getWorld() { + return this.world; + } +@@ -274,7 +318,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey getTypeKey(); + +- protected final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(java.util.concurrent.ThreadLocalRandom.current().nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - move thread unsafe random initialization // Pufferfish - getter ++ //protected final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(java.util.concurrent.ThreadLocalRandom.current().nextLong()); public net.minecraft.util.RandomSource getThreadUnsafeRandom() { return this.randomTickRandom; } // Pufferfish - move thread unsafe random initialization // Pufferfish - getter // Purpur - dont break ABI + + // Pufferfish start - ensure these get inlined + private final int minBuildHeight, minSection, height, maxBuildHeight, maxSection; +@@ -288,6 +332,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper ++ this.purpurConfig = new org.purpurmc.purpur.PurpurWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), env); // Purpur ++ this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); // Purpur + this.generator = gen; + this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env); + +@@ -672,9 +718,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + BlockState iblockdata2 = this.getBlockState(pos); + + if ((flags & 128) == 0 && iblockdata2 != iblockdata1 && (iblockdata2.getLightBlock(this, pos) != iblockdata1.getLightBlock(this, pos) || iblockdata2.getLightEmission() != iblockdata1.getLightEmission() || iblockdata2.useShapeForLightOcclusion() || iblockdata1.useShapeForLightOcclusion())) { +- this.getProfiler().push("queueCheckLight"); ++ //this.getProfiler().push("queueCheckLight"); // Purpur + this.getChunkSource().getLightEngine().checkBlock(pos); +- this.getProfiler().pop(); ++ //this.getProfiler().pop(); // Purpur + } + + /* +@@ -973,18 +1019,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + protected void tickBlockEntities() { +- ProfilerFiller gameprofilerfiller = this.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = this.getProfiler(); // Purpur + +- gameprofilerfiller.push("blockEntities"); +- timings.tileEntityPending.startTiming(); // Spigot ++ //gameprofilerfiller.push("blockEntities"); // Purpur ++ //timings.tileEntityPending.startTiming(); // Spigot // Purpur + this.tickingBlockEntities = true; + if (!this.pendingBlockEntityTickers.isEmpty()) { + this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); + this.pendingBlockEntityTickers.clear(); + } +- timings.tileEntityPending.stopTiming(); // Spigot ++ //timings.tileEntityPending.stopTiming(); // Spigot // Purpur + +- timings.tileEntityTick.startTiming(); // Spigot ++ //timings.tileEntityTick.startTiming(); // Spigot // Purpur + // Spigot start + // Iterator iterator = this.blockEntityTickers.iterator(); + int tilesThisCycle = 0; +@@ -1017,10 +1063,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + this.blockEntityTickers.removeAll(toRemove); + +- timings.tileEntityTick.stopTiming(); // Spigot ++ //timings.tileEntityTick.stopTiming(); // Spigot // Purpur + this.tickingBlockEntities = false; + co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + spigotConfig.currentPrimedTnt = 0; // Spigot + } + +@@ -1213,7 +1259,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { +- this.getProfiler().incrementCounter("getEntities"); ++ //this.getProfiler().incrementCounter("getEntities"); // Purpur + List list = Lists.newArrayList(); + ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call + return list; +@@ -1232,7 +1278,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { +- this.getProfiler().incrementCounter("getEntities"); ++ //this.getProfiler().incrementCounter("getEntities"); // Purpur + // Paper start - optimise this call + //TODO use limit + if (filter instanceof net.minecraft.world.entity.EntityType entityTypeTest) { +@@ -1557,7 +1603,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public ProfilerFiller getProfiler() { +- if (gg.pufferfish.pufferfish.PufferfishConfig.disableMethodProfiler) return net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Pufferfish ++ if (true || gg.pufferfish.pufferfish.PufferfishConfig.disableMethodProfiler) return net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Pufferfish + return (ProfilerFiller) this.profiler.get(); + } + +@@ -1648,4 +1694,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return null; + } + // Paper end ++ ++ // Purpur start ++ public boolean isNether() { ++ return getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER; ++ } ++ ++ public boolean isTheEnd() { ++ return getWorld().getEnvironment() == org.bukkit.World.Environment.THE_END; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 6180679d922ea61d05d452971ec2d506a724d3c3..132041362b2707946bd386c88bbdf871a317afb7 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -132,8 +132,8 @@ public final class NaturalSpawner { + } + + public static void spawnForChunk(ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnState info, boolean spawnAnimals, boolean spawnMonsters, boolean rareSpawn) { +- world.getProfiler().push("spawner"); +- world.timings.mobSpawn.startTiming(); // Spigot ++ //world.getProfiler().push("spawner"); // Purpur ++ //world.timings.mobSpawn.startTiming(); // Spigot // Purpur + MobCategory[] aenumcreaturetype = NaturalSpawner.SPAWNING_CATEGORIES; + int i = aenumcreaturetype.length; + +@@ -188,8 +188,8 @@ public final class NaturalSpawner { + } + } + +- world.timings.mobSpawn.stopTiming(); // Spigot +- world.getProfiler().pop(); ++ //world.timings.mobSpawn.stopTiming(); // Spigot // Purpur ++ //world.getProfiler().pop(); // Purpur + } + + // Paper start +@@ -260,7 +260,7 @@ public final class NaturalSpawner { + blockposition_mutableblockposition.set(l, i, i1); + double d0 = (double) l + 0.5D; + double d1 = (double) i1 + 0.5D; +- Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); // Paper - use chunk's player cache to optimize search in range ++ Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, world.purpurConfig.mobSpawningIgnoreCreativePlayers ? net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR : net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, world.purpurConfig.mobSpawningIgnoreCreativePlayers); // Paper - use chunk's player cache to optimize search in range // Purpur + + if (entityhuman != null) { + double d2 = entityhuman.distanceToSqr(d0, (double) i, d1); +diff --git a/src/main/java/net/minecraft/world/level/block/AnvilBlock.java b/src/main/java/net/minecraft/world/level/block/AnvilBlock.java +index 5c5a3b169795bf8a527b316c666cbc2105c66622..020afeca950d2c7fb6c7b179d424548fd90f8b0d 100644 +--- a/src/main/java/net/minecraft/world/level/block/AnvilBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/AnvilBlock.java +@@ -55,6 +55,54 @@ public class AnvilBlock extends FallingBlock { + + @Override + public InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { ++ // Purpur start - repairable/damageable anvils ++ if (world.purpurConfig.anvilRepairIngotsAmount > 0) { ++ net.minecraft.world.item.ItemStack itemstack = player.getItemInHand(hand); ++ if (itemstack.is(net.minecraft.world.item.Items.IRON_INGOT)) { ++ if (itemstack.getCount() < world.purpurConfig.anvilRepairIngotsAmount) { ++ // not enough iron ingots, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ if (state.is(Blocks.DAMAGED_ANVIL)) { ++ world.setBlock(pos, Blocks.CHIPPED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.CHIPPED_ANVIL)) { ++ world.setBlock(pos, Blocks.ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.ANVIL)) { ++ // anvil is already fully repaired, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(world.purpurConfig.anvilRepairIngotsAmount); ++ } ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_PLACE, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ } ++ if (world.purpurConfig.anvilDamageObsidianAmount > 0) { ++ net.minecraft.world.item.ItemStack itemstack = player.getItemInHand(hand); ++ if (itemstack.is(net.minecraft.world.item.Items.OBSIDIAN)) { ++ if (itemstack.getCount() < world.purpurConfig.anvilDamageObsidianAmount) { ++ // not enough obsidian, play "error" sound and consume ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_HIT, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ if (state.is(Blocks.DAMAGED_ANVIL)) { ++ world.destroyBlock(pos, false); ++ } else if (state.is(Blocks.CHIPPED_ANVIL)) { ++ world.setBlock(pos, Blocks.DAMAGED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } else if (state.is(Blocks.ANVIL)) { ++ world.setBlock(pos, Blocks.CHIPPED_ANVIL.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3); ++ } ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(world.purpurConfig.anvilDamageObsidianAmount); ++ } ++ world.playSound(null, pos, net.minecraft.sounds.SoundEvents.ANVIL_LAND, net.minecraft.sounds.SoundSource.BLOCKS, 1.0F, 1.0F); ++ return InteractionResult.CONSUME; ++ } ++ } ++ // Purpur end + if (world.isClientSide) { + return InteractionResult.SUCCESS; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java b/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java +index 95b53450a807fccfa55b59852da52785b8cf3e3d..c69f1d23979a0759472d22760a18d986b2d979b6 100644 +--- a/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/AzaleaBlock.java +@@ -43,6 +43,20 @@ public class AzaleaBlock extends BushBlock implements BonemealableBlock { + + @Override + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ // Purpur start ++ growTree(world, random, pos, state); ++ } ++ ++ @Override ++ public void randomTick(net.minecraft.world.level.block.state.BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { ++ double chance = state.getBlock() == Blocks.FLOWERING_AZALEA ? world.purpurConfig.floweringAzaleaGrowthChance : world.purpurConfig.azaleaGrowthChance; ++ if (chance > 0.0D && world.getMaxLocalRawBrightness(pos.above()) > 9 && random.nextDouble() < chance) { ++ growTree(world, random, pos, state); ++ } ++ } ++ ++ private void growTree(ServerLevel world, RandomSource random, BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { ++ // Purpur end + TREE_GROWER.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java b/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java +index 3d2b34c5a7c9b00c1164b4f89c2cbff81fc460eb..b5505e926e5cdb447de68e8eb8e46c97eb988e27 100644 +--- a/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BaseCoralPlantTypeBlock.java +@@ -35,6 +35,7 @@ public class BaseCoralPlantTypeBlock extends Block implements SimpleWaterloggedB + } + + protected static boolean scanForWater(BlockState state, BlockGetter world, BlockPos pos) { ++ if (!((net.minecraft.world.level.LevelAccessor) world).getMinecraftWorld().purpurConfig.coralDieOutsideWater) return true; // Purpur + if (state.getValue(WATERLOGGED)) { + return true; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/BedBlock.java b/src/main/java/net/minecraft/world/level/block/BedBlock.java +index d1d5363ab1742add8ff45507a303106f4d65f52f..19d31064eb271ee02115a75cde383796c899e7f7 100644 +--- a/src/main/java/net/minecraft/world/level/block/BedBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BedBlock.java +@@ -97,7 +97,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + Vec3 vec3d = pos.getCenter(); + +- world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); ++ if (world.purpurConfig.bedExplode) world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, (float) world.purpurConfig.bedExplosionPower, world.purpurConfig.bedExplosionFire, world.purpurConfig.bedExplosionEffect); // Purpur + return InteractionResult.SUCCESS; + } else if ((Boolean) state.getValue(BedBlock.OCCUPIED)) { + if (!BedBlock.canSetSpawn(world)) return this.explodeBed(state, world, pos); // Paper - check explode first +@@ -150,7 +150,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + Vec3 vec3d = blockposition.getCenter(); + +- world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); ++ if (world.purpurConfig.bedExplode) world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, explodedBlockState), (ExplosionDamageCalculator) null, vec3d, (float) world.purpurConfig.bedExplosionPower, world.purpurConfig.bedExplosionFire, world.purpurConfig.bedExplosionEffect); // Purpur + return InteractionResult.SUCCESS; + } + } +@@ -174,7 +174,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- super.fallOn(world, state, pos, entity, fallDistance * 0.5F); ++ super.fallOn(world, state, pos, entity, fallDistance); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java b/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java +index 8537581e7ca1f4efb492a2e734f46f947f36cffa..5f89229ff68d923c5cdee40e72e379ba7024f961 100644 +--- a/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BigDripleafBlock.java +@@ -236,7 +236,7 @@ public class BigDripleafBlock extends HorizontalDirectionalBlock implements Bone + BigDripleafBlock.playTiltSound(world, blockposition, soundeffect); + } + +- int i = BigDripleafBlock.DELAY_UNTIL_NEXT_TILT_STATE.getInt(tilt); ++ int i = world.purpurConfig.bigDripleafTiltDelay.getOrDefault(tilt, -1); // Purpur + + if (i != -1) { + world.scheduleTick(blockposition, (Block) this, i); +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index 4f91e4832a94c3facbc711fcae4cb5ad540a5ca0..773162c3456945605fb664114508622f7d2fcec8 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -63,6 +63,13 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + import org.slf4j.Logger; + ++// Purpur start ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.StringTag; ++import net.minecraft.world.Nameable; ++// Purpur end ++ + public class Block extends BlockBehaviour implements ItemLike { + + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -89,6 +96,10 @@ public class Block extends BlockBehaviour implements ItemLike { + public static final int UPDATE_LIMIT = 512; + protected final StateDefinition stateDefinition; + private BlockState defaultBlockState; ++ // Purpur start ++ public float fallDamageMultiplier = 1.0F; ++ public float fallDistanceMultiplier = 1.0F; ++ // Purpur end + // Paper start + public final boolean isDestroyable() { + return io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || +@@ -325,7 +336,7 @@ public class Block extends BlockBehaviour implements ItemLike { + public static void dropResources(BlockState state, LevelAccessor world, BlockPos pos, @Nullable BlockEntity blockEntity) { + if (world instanceof ServerLevel) { + Block.getDrops(state, (ServerLevel) world, pos, blockEntity).forEach((itemstack) -> { +- Block.popResource((ServerLevel) world, pos, itemstack); ++ Block.popResource((ServerLevel) world, pos, applyDisplayNameAndLoreFromTile(itemstack, blockEntity)); // Purpur + }); + state.spawnAfterBreak((ServerLevel) world, pos, ItemStack.EMPTY, true); + } +@@ -341,7 +352,7 @@ public class Block extends BlockBehaviour implements ItemLike { + io.papermc.paper.event.block.BlockBreakBlockEvent event = new io.papermc.paper.event.block.BlockBreakBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(world, pos), org.bukkit.craftbukkit.block.CraftBlock.at(world, source), items); + event.callEvent(); + for (var drop : event.getDrops()) { +- popResource(world.getMinecraftWorld(), pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop)); ++ popResource(world.getMinecraftWorld(), pos, applyDisplayNameAndLoreFromTile(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop), blockEntity)); // Purpur + } + state.spawnAfterBreak(world.getMinecraftWorld(), pos, ItemStack.EMPTY, true); + } +@@ -352,13 +363,53 @@ public class Block extends BlockBehaviour implements ItemLike { + public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, Entity entity, ItemStack tool) { + if (world instanceof ServerLevel) { + Block.getDrops(state, (ServerLevel) world, pos, blockEntity, entity, tool).forEach((itemstack1) -> { +- Block.popResource(world, pos, itemstack1); ++ Block.popResource(world, pos, applyDisplayNameAndLoreFromTile(itemstack1, blockEntity)); // Purpur + }); + state.spawnAfterBreak((ServerLevel) world, pos, tool, true); + } + + } + ++ // Purpur start ++ private static ItemStack applyDisplayNameAndLoreFromTile(ItemStack stack, BlockEntity blockEntity) { ++ if (stack.getItem() instanceof BlockItem) { ++ if (blockEntity != null && blockEntity.getLevel() instanceof ServerLevel && blockEntity.getLevel().purpurConfig.persistentTileEntityDisplayNames) { ++ String name = blockEntity.getPersistentDisplayName(); ++ ListTag lore = blockEntity.getPersistentLore(); ++ if (blockEntity instanceof Nameable) { ++ Nameable namedTile = (Nameable) blockEntity; ++ if (namedTile.hasCustomName()) { ++ name = Component.Serializer.toJson(namedTile.getCustomName()); ++ } ++ } ++ ++ if (name != null || lore != null) { ++ CompoundTag display = stack.getTagElement("display"); ++ if (display == null) { ++ display = new CompoundTag(); ++ } ++ ++ if (name != null) { ++ display.put("Name", StringTag.valueOf(name)); ++ } ++ if (lore != null) { ++ display.put("Lore", lore); ++ } ++ ++ CompoundTag tag = stack.getTag(); ++ if (tag == null) { ++ tag = new CompoundTag(); ++ } ++ tag.put("display", display); ++ ++ stack.setTag(tag); ++ } ++ } ++ } ++ return stack; ++ } ++ // Purpur end ++ + public static void popResource(Level world, BlockPos pos, ItemStack stack) { + double d0 = (double) EntityType.ITEM.getHeight() / 2.0D; + double d1 = (double) pos.getX() + 0.5D + Mth.nextDouble(world.random, -0.25D, 0.25D); +@@ -434,7 +485,17 @@ public class Block extends BlockBehaviour implements ItemLike { + Block.dropResources(state, world, pos, blockEntity, player, tool); + } + +- public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) {} ++ // Purpur start ++ @Nullable protected LivingEntity placer = null; ++ ++ public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) { ++ this.placer = placer; ++ } ++ ++ public void forgetPlacer() { ++ this.placer = null; ++ } ++ // Purpur end + + public boolean isPossibleToRespawnInThis() { + return !this.material.isSolid() && !this.material.isLiquid(); +@@ -453,7 +514,7 @@ public class Block extends BlockBehaviour implements ItemLike { + } + + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- entity.causeFallDamage(fallDistance, 1.0F, entity.damageSources().fall()); ++ entity.causeFallDamage(fallDistance * fallDistanceMultiplier, fallDamageMultiplier, entity.damageSources().fall()); // Purpur + } + + public void updateEntityAfterFallOn(BlockGetter world, Entity entity) { +diff --git a/src/main/java/net/minecraft/world/level/block/Blocks.java b/src/main/java/net/minecraft/world/level/block/Blocks.java +index f148c7d2954cc17377d0da4af03ea2c1c9397a52..4afc4670f9b00a4087410ec366fe45fe2f2734dc 100644 +--- a/src/main/java/net/minecraft/world/level/block/Blocks.java ++++ b/src/main/java/net/minecraft/world/level/block/Blocks.java +@@ -1087,8 +1087,8 @@ public class Blocks { + public static final Block CAVE_VINES = register("cave_vines", new CaveVinesBlock(BlockBehaviour.Properties.of(Material.PLANT).randomTicks().noCollission().lightLevel(CaveVines.emission(14)).instabreak().sound(SoundType.CAVE_VINES))); + public static final Block CAVE_VINES_PLANT = register("cave_vines_plant", new CaveVinesPlantBlock(BlockBehaviour.Properties.of(Material.PLANT).noCollission().lightLevel(CaveVines.emission(14)).instabreak().sound(SoundType.CAVE_VINES))); + public static final Block SPORE_BLOSSOM = register("spore_blossom", new SporeBlossomBlock(BlockBehaviour.Properties.of(Material.PLANT).instabreak().noCollission().sound(SoundType.SPORE_BLOSSOM))); +- public static final Block AZALEA = register("azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).instabreak().sound(SoundType.AZALEA).noOcclusion())); +- public static final Block FLOWERING_AZALEA = register("flowering_azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).instabreak().sound(SoundType.FLOWERING_AZALEA).noOcclusion())); ++ public static final Block AZALEA = register("azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).randomTicks().instabreak().sound(SoundType.AZALEA).noOcclusion())); // Purpur ++ public static final Block FLOWERING_AZALEA = register("flowering_azalea", new AzaleaBlock(BlockBehaviour.Properties.of(Material.PLANT).randomTicks().instabreak().sound(SoundType.FLOWERING_AZALEA).noOcclusion())); // Purpur + public static final Block MOSS_CARPET = register("moss_carpet", new CarpetBlock(BlockBehaviour.Properties.of(Material.PLANT, MaterialColor.COLOR_GREEN).strength(0.1F).sound(SoundType.MOSS_CARPET))); + public static final Block PINK_PETALS = register("pink_petals", new PinkPetalsBlock(BlockBehaviour.Properties.of(Material.PLANT).noCollission().sound(SoundType.PINK_PETALS).requiredFeatures(FeatureFlags.UPDATE_1_20))); + public static final Block MOSS_BLOCK = register("moss_block", new MossBlock(BlockBehaviour.Properties.of(Material.MOSS, MaterialColor.COLOR_GREEN).strength(0.1F).sound(SoundType.MOSS))); +@@ -1153,7 +1153,7 @@ public class Blocks { + } + + private static Boolean ocelotOrParrot(BlockState state, BlockGetter world, BlockPos pos, EntityType type) { +- return (boolean)type == EntityType.OCELOT || type == EntityType.PARROT; ++ return type == EntityType.OCELOT || type == EntityType.PARROT; // Purpur - decompile error + } + + private static BedBlock bed(DyeColor color) { +diff --git a/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java b/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java +index bedccb8717d08d5a60058445b04ddff149e7d36c..5293ffca3da94c9c485a87d1232b6a902fcafd6a 100644 +--- a/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java +@@ -53,4 +53,14 @@ public class BuddingAmethystBlock extends AmethystBlock { + public static boolean canClusterGrowAtState(BlockState state) { + return state.isAir() || state.is(Blocks.WATER) && state.getFluidState().getAmount() == 8; + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(net.minecraft.world.level.Level level, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, net.minecraft.world.level.block.entity.BlockEntity blockEntity, net.minecraft.world.item.ItemStack stack) { ++ if (level.purpurConfig.buddingAmethystSilkTouch && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH, stack) > 0) { ++ popResource(level, pos, net.minecraft.world.item.Items.BUDDING_AMETHYST.getDefaultInstance()); ++ } ++ super.playerDestroy(level, player, pos, state, blockEntity, stack); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java +index 03fde6e47c4a347c62fe9b4a3351769aedf874f6..ca906b0250e5332f7ececf1419ca6d2c1d385adc 100644 +--- a/src/main/java/net/minecraft/world/level/block/BushBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java +@@ -48,4 +48,24 @@ public class BushBlock extends Block { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return type == PathComputationType.AIR && !this.hasCollision ? true : super.isPathfindable(state, world, pos, type); + } ++ ++ // Purpur start ++ public void playerDestroyAndReplant(net.minecraft.world.level.Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, net.minecraft.world.item.ItemStack itemInHand, net.minecraft.world.level.ItemLike itemToReplant) { ++ player.awardStat(net.minecraft.stats.Stats.BLOCK_MINED.get(this)); ++ player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); ++ java.util.List dropList = Block.getDrops(state, (net.minecraft.server.level.ServerLevel) world, pos, blockEntity, player, itemInHand); ++ ++ boolean planted = false; ++ for (net.minecraft.world.item.ItemStack itemToDrop : dropList) { ++ if (!planted && itemToDrop.getItem() == itemToReplant) { ++ world.setBlock(pos, defaultBlockState(), 3); ++ itemToDrop.setCount(itemToDrop.getCount() - 1); ++ planted = true; ++ } ++ Block.popResource(world, pos, itemToDrop); ++ } ++ ++ state.spawnAfterBreak((net.minecraft.server.level.ServerLevel) world, pos, itemInHand, true); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +index 7579946ce222b6ab3685a7fd9821bcd5a4babe33..ae2ac1c24c1e502a1968a3008273096281d5f1ca 100644 +--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +@@ -22,7 +22,7 @@ import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit + +-public class CactusBlock extends Block { ++public class CactusBlock extends Block implements BonemealableBlock { // Purpur + + public static final IntegerProperty AGE = BlockStateProperties.AGE_15; + public static final int MAX_AGE = 15; +@@ -109,7 +109,7 @@ public class CactusBlock extends Block { + BlockState iblockdata2 = world.getBlockState(pos.relative(enumdirection)); + + material = iblockdata2.getMaterial(); +- } while (!material.isSolid() && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); ++ } while ((!world.getWorldBorder().world.purpurConfig.cactusBreaksFromSolidNeighbors || !material.isSolid()) && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); // Purpur + + return false; + } +@@ -131,4 +131,34 @@ public class CactusBlock extends Block { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public boolean isValidBonemealTarget(LevelReader world, BlockPos pos, BlockState state, boolean isClient) { ++ if (!((Level) world).purpurConfig.cactusAffectedByBonemeal || !world.isEmptyBlock(pos.above())) return false; ++ ++ int cactusHeight = 0; ++ while (world.getBlockState(pos.below(cactusHeight)).is(this)) { ++ cactusHeight++; ++ } ++ ++ return cactusHeight < ((Level) world).paperConfig().maxGrowthHeight.cactus; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int cactusHeight = 0; ++ while (world.getBlockState(pos.below(cactusHeight)).is(this)) { ++ cactusHeight++; ++ } ++ for (int i = 0; i <= world.paperConfig().maxGrowthHeight.cactus - cactusHeight; i++) { ++ world.setBlockAndUpdate(pos.above(i), state.setValue(CactusBlock.AGE, 0)); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +index 219c87dcf065e86512f330fbeec59e55f4675083..f8fd3b320494d1c1e8ee3d170f2feebd152230fa 100644 +--- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +@@ -122,7 +122,7 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB + BlockPos blockposition = ctx.getClickedPos(); + boolean flag = world.getFluidState(blockposition).getType() == Fluids.WATER; + +- return (BlockState) ((BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(CampfireBlock.WATERLOGGED, flag)).setValue(CampfireBlock.SIGNAL_FIRE, this.isSmokeSource(world.getBlockState(blockposition.below())))).setValue(CampfireBlock.LIT, !flag)).setValue(CampfireBlock.FACING, ctx.getHorizontalDirection()); ++ return (BlockState) ((BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(CampfireBlock.WATERLOGGED, flag)).setValue(CampfireBlock.SIGNAL_FIRE, this.isSmokeSource(world.getBlockState(blockposition.below())))).setValue(CampfireBlock.LIT, world.purpurConfig.campFireLitWhenPlaced ? !flag : world.purpurConfig.campFireLitWhenPlaced)).setValue(CampfireBlock.FACING, ctx.getHorizontalDirection()); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java b/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java +index 05112bc416019daba885a3de1b7f96177665135f..32d7ae44dd4e4987b1085f08cb30a92937e57226 100644 +--- a/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java +@@ -69,7 +69,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock implements Eq + SnowGolem entitysnowman = (SnowGolem) EntityType.SNOW_GOLEM.create(world); + + if (entitysnowman != null) { +- CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection, entitysnowman, shapedetector_shapedetectorcollection.getBlock(0, 2, 0).getPos()); ++ CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection, entitysnowman, shapedetector_shapedetectorcollection.getBlock(0, 2, 0).getPos(), this.placer); // Purpur + } + } else { + BlockPattern.BlockPatternMatch shapedetector_shapedetectorcollection1 = this.getOrCreateIronGolemFull().find(world, pos); +@@ -79,7 +79,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock implements Eq + + if (entityirongolem != null) { + entityirongolem.setPlayerCreated(true); +- CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection1, entityirongolem, shapedetector_shapedetectorcollection1.getBlock(1, 2, 0).getPos()); ++ CarvedPumpkinBlock.spawnGolemInWorld(world, shapedetector_shapedetectorcollection1, entityirongolem, shapedetector_shapedetectorcollection1.getBlock(1, 2, 0).getPos(), this.placer); // Purpur + } + } + } +@@ -87,6 +87,16 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock implements Eq + } + + private static void spawnGolemInWorld(Level world, BlockPattern.BlockPatternMatch patternResult, Entity entity, BlockPos pos) { ++ // Purpur start ++ spawnGolemInWorld(world, patternResult, entity, pos, null); ++ } ++ private static void spawnGolemInWorld(Level world, BlockPattern.BlockPatternMatch patternResult, Entity entity, BlockPos pos, net.minecraft.world.entity.LivingEntity placer) { ++ if (entity instanceof SnowGolem snowGolem) { ++ snowGolem.setSummoner(placer == null ? null : placer.getUUID()); ++ } else if (entity instanceof IronGolem ironGolem) { ++ ironGolem.setSummoner(placer == null ? null : placer.getUUID()); ++ } ++ // Purpur end + // clearPatternBlocks(world, shapedetector_shapedetectorcollection); // CraftBukkit - moved down + entity.moveTo((double) pos.getX() + 0.5D, (double) pos.getY() + 0.05D, (double) pos.getZ() + 0.5D, 0.0F, 0.0F); + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/world/level/block/CauldronBlock.java b/src/main/java/net/minecraft/world/level/block/CauldronBlock.java +index 2f85b893dd0abc39fcedec65acc89e1567faf6f0..3ee012a9ef8cada0b2203e53b2f731f60f697cb1 100644 +--- a/src/main/java/net/minecraft/world/level/block/CauldronBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CauldronBlock.java +@@ -29,7 +29,7 @@ public class CauldronBlock extends AbstractCauldronBlock { + } + + protected static boolean shouldHandlePrecipitation(Level world, Biome.Precipitation precipitation) { +- return precipitation == Biome.Precipitation.RAIN ? world.getRandom().nextFloat() < 0.05F : (precipitation == Biome.Precipitation.SNOW ? world.getRandom().nextFloat() < 0.1F : false); ++ return precipitation == Biome.Precipitation.RAIN ? world.getRandom().nextFloat() < world.purpurConfig.cauldronRainChance : (precipitation == Biome.Precipitation.SNOW ? world.getRandom().nextFloat() < world.purpurConfig.cauldronPowderSnowChance : false); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java b/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java +index 18b5bce1138d50be32e5da013221be69dc47e21f..58b4a0d97af37f7164db86ef821f04102c6c5ddd 100644 +--- a/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CaveVinesBlock.java +@@ -88,4 +88,11 @@ public class CaveVinesBlock extends GrowingPlantHeadBlock implements Bonemealabl + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { + world.setBlock(pos, state.setValue(BERRIES, Boolean.valueOf(true)), 2); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.caveVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/ChestBlock.java b/src/main/java/net/minecraft/world/level/block/ChestBlock.java +index 5e22d175b1048a58802cdf64ac70a8b56329e915..d81946b400f208c39941128ce823ff7709741c10 100644 +--- a/src/main/java/net/minecraft/world/level/block/ChestBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ChestBlock.java +@@ -355,6 +355,7 @@ public class ChestBlock extends AbstractChestBlock implements + } + + private static boolean isBlockedChestByBlock(BlockGetter world, BlockPos pos) { ++ if (world instanceof Level && ((Level) world).purpurConfig.chestOpenWithBlockOnTop) return false; // Purpur + BlockPos blockposition1 = pos.above(); + + return world.getBlockState(blockposition1).isRedstoneConductor(world, blockposition1); +diff --git a/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java b/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java +index a6c25647fb37f59307de0d390f8e8cf55504d7d3..52aae8bd4023b2bb48f12983f54b20fa3c95d403 100644 +--- a/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ChorusPlantBlock.java +@@ -21,6 +21,7 @@ public class ChorusPlantBlock extends PipeBlock { + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { ++ if (org.purpurmc.purpur.PurpurConfig.disableChorusPlantUpdates) return this.defaultBlockState(); // Purpur + return this.getStateForPlacement(ctx.getLevel(), ctx.getClickedPos()); + } + +@@ -36,6 +37,7 @@ public class ChorusPlantBlock extends PipeBlock { + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { ++ if (org.purpurmc.purpur.PurpurConfig.disableChorusPlantUpdates) return state; // Purpur + if (!state.canSurvive(world, pos)) { + world.scheduleTick(pos, this, 1); + return super.updateShape(state, direction, neighborState, world, pos, neighborPos); +diff --git a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +index fb4382337fe83f7d00c2212a7a71e0ba5bdd51cc..f085a669e2f2645e8c4f7a7e5a3c958f13809744 100644 +--- a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +@@ -228,26 +228,28 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + ItemStack itemstack = player.getItemInHand(hand); + + if (i < 8 && ComposterBlock.COMPOSTABLES.containsKey(itemstack.getItem())) { +- if (i < 7 && !world.isClientSide) { +- // Paper start - EntityChangeBlockEvent +- double rand = world.getRandom().nextDouble(); +- BlockState dummyBlockState = ComposterBlock.addItem(player, state, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, pos, itemstack, rand); +- if (dummyBlockState == null) { +- return InteractionResult.PASS; +- } +- if (state != dummyBlockState && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, pos, dummyBlockState).isCancelled()) { // if block state will change and event cancelled +- return InteractionResult.sidedSuccess(world.isClientSide); +- } +- BlockState iblockdata1 = ComposterBlock.addItem(player, state, world, pos, itemstack, rand); +- // Paper end +- +- world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0); +- player.awardStat(Stats.ITEM_USED.get(itemstack.getItem())); +- if (!player.getAbilities().instabuild) { +- itemstack.shrink(1); +- } ++ // Purpur start ++ BlockState newState = process(i, state, world, itemstack, pos, player); ++ if (newState == null) { ++ return InteractionResult.PASS; + } + ++ if (world.purpurConfig.composterBulkProcess && player.isShiftKeyDown() && newState != state) { ++ BlockState oldState; ++ int oldCount, newCount, oldLevel, newLevel; ++ do { ++ oldState = newState; ++ oldCount = itemstack.getCount(); ++ oldLevel = oldState.getValue(ComposterBlock.LEVEL); ++ newState = process(oldLevel, oldState, world, itemstack, pos, player); ++ if (newState == null) { ++ return InteractionResult.PASS; ++ } ++ newCount = itemstack.getCount(); ++ newLevel = newState.getValue(ComposterBlock.LEVEL); ++ } while (newCount > 0 && (newCount != oldCount || newLevel != oldLevel || newState != oldState)); ++ } ++ // Purpur end + return InteractionResult.sidedSuccess(world.isClientSide); + } else if (i == 8) { + ComposterBlock.extractProduce(player, state, world, pos); +@@ -257,6 +259,32 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + ++ // Purpur start ++ private static BlockState process(int level, BlockState state, Level world, ItemStack itemstack, BlockPos pos, Player player) { ++ if (level < 7 && !world.isClientSide) { ++ // Paper start - EntityChangeBlockEvent ++ double rand = world.getRandom().nextDouble(); ++ BlockState dummyBlockState = ComposterBlock.addItem(player, state, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, pos, itemstack, rand); ++ if (dummyBlockState == null) { ++ return dummyBlockState; ++ } ++ if (state != dummyBlockState && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, pos, dummyBlockState).isCancelled()) { // if block state will change and event cancelled ++ return state; ++ } ++ BlockState iblockdata1 = ComposterBlock.addItem(player, state, world, pos, itemstack, rand); ++ // Paper end ++ ++ world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0); ++ player.awardStat(Stats.ITEM_USED.get(itemstack.getItem())); ++ if (!player.getAbilities().instabuild) { ++ itemstack.shrink(1); ++ } ++ return dummyBlockState; ++ } ++ return state; ++ } ++ // Purpur end ++ + public static BlockState insertItem(Entity user, BlockState state, ServerLevel world, ItemStack stack, BlockPos pos) { + int i = (Integer) state.getValue(ComposterBlock.LEVEL); + +diff --git a/src/main/java/net/minecraft/world/level/block/CoralBlock.java b/src/main/java/net/minecraft/world/level/block/CoralBlock.java +index 88faea00be60a519f56f975a5311df5e1eb3e6b8..cbb726ac367be81e27d3a86643baf7c4f0746edf 100644 +--- a/src/main/java/net/minecraft/world/level/block/CoralBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CoralBlock.java +@@ -45,6 +45,7 @@ public class CoralBlock extends Block { + } + + protected boolean scanForWater(BlockGetter world, BlockPos pos) { ++ if (!((net.minecraft.world.level.LevelAccessor) world).getMinecraftWorld().purpurConfig.coralDieOutsideWater) return true; // Purpur + Direction[] aenumdirection = Direction.values(); + int i = aenumdirection.length; + +diff --git a/src/main/java/net/minecraft/world/level/block/CropBlock.java b/src/main/java/net/minecraft/world/level/block/CropBlock.java +index a140fed067e7e6c1c42e111f47d3678863ef95ce..3415cbb1def0700b5998a8a1db2e48146f4c2c1e 100644 +--- a/src/main/java/net/minecraft/world/level/block/CropBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CropBlock.java +@@ -168,7 +168,7 @@ public class CropBlock extends BushBlock implements BonemealableBlock { + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper +- if (entity instanceof Ravager && !CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)).isCancelled()) { // CraftBukkit ++ if (entity instanceof Ravager && world.purpurConfig.ravagerGriefableBlocks.contains(world.getBlockState(pos).getBlock()) && !CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), (!world.purpurConfig.ravagerBypassMobGriefing && !world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))).isCancelled()) { // CraftBukkit // Purpur + world.destroyBlock(pos, true, entity); + } + +@@ -203,4 +203,15 @@ public class CropBlock extends BushBlock implements BonemealableBlock { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(CropBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, ItemStack itemInHand) { ++ if (world.purpurConfig.hoeReplantsCrops && itemInHand.getItem() instanceof net.minecraft.world.item.HoeItem) { ++ super.playerDestroyAndReplant(world, player, pos, state, blockEntity, itemInHand, getBaseSeedId()); ++ } else { ++ super.playerDestroy(world, player, pos, state, blockEntity, itemInHand); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/DoorBlock.java b/src/main/java/net/minecraft/world/level/block/DoorBlock.java +index 5ba56ee7d5dd210770e6703be559055d218028d5..b5e90dc00240bccf1a6eca342729a4f4165e22bf 100644 +--- a/src/main/java/net/minecraft/world/level/block/DoorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DoorBlock.java +@@ -165,6 +165,7 @@ public class DoorBlock extends Block { + public InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (this.material == Material.METAL) { + return InteractionResult.PASS; ++ } else if (requiresRedstone(world, state, pos)) { return InteractionResult.CONSUME; // Purpur + } else { + state = (BlockState) state.cycle(DoorBlock.OPEN); + world.setBlock(pos, state, 10); +@@ -260,4 +261,18 @@ public class DoorBlock extends Block { + public static boolean isWoodenDoor(BlockState state) { + return state.getBlock() instanceof DoorBlock && (state.getMaterial() == Material.WOOD || state.getMaterial() == Material.NETHER_WOOD); + } ++ ++ // Purpur start ++ public static boolean requiresRedstone(Level level, BlockState state, BlockPos pos) { ++ if (level.purpurConfig.doorRequiresRedstone.contains(state.getBlock())) { ++ // force update client ++ BlockPos otherPos = pos.relative(state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN); ++ BlockState otherState = level.getBlockState(otherPos); ++ level.sendBlockUpdated(pos, state, state, 3); ++ level.sendBlockUpdated(otherPos, otherState, otherState, 3); ++ return true; ++ } ++ return false; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java b/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java +index 7e1edcc7b9f170b7c649437c2f0dd78c0bab9be4..5f8ac1fdac2c334951261f2b9702f5e711743c88 100644 +--- a/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DragonEggBlock.java +@@ -42,8 +42,8 @@ public class DragonEggBlock extends FallingBlock { + } + + private void teleport(BlockState state, Level world, BlockPos pos) { ++ if (!world.purpurConfig.dragonEggTeleport) return; // Purpur + WorldBorder worldborder = world.getWorldBorder(); +- + for (int i = 0; i < 1000; ++i) { + BlockPos blockposition1 = pos.offset(world.random.nextInt(16) - world.random.nextInt(16), world.random.nextInt(8) - world.random.nextInt(8), world.random.nextInt(16) - world.random.nextInt(16)); + +diff --git a/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java b/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java +index f4ee3ce287528337a0f9a3b612c157254f895a58..c4a91d7f1320027ee6a2b364303c01ebbacde584 100644 +--- a/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java +@@ -28,6 +28,8 @@ import net.minecraft.world.level.pathfinder.PathComputationType; + import net.minecraft.world.phys.BlockHitResult; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; ++import net.minecraft.world.Containers; // Purpur ++import net.minecraft.world.item.Items; // Purpur + + public class EnchantmentTableBlock extends BaseEntityBlock { + protected static final VoxelShape SHAPE = Block.box(0.0D, 0.0D, 0.0D, 16.0D, 12.0D, 16.0D); +@@ -40,6 +42,10 @@ public class EnchantmentTableBlock extends BaseEntityBlock { + } + + public static boolean isValidBookShelf(Level world, BlockPos tablePos, BlockPos bookshelfOffset) { ++ // Purpur Start ++ if(org.purpurmc.purpur.PurpurConfig.allowTransparentBlocksInEnchantmentBox){ ++ return world.getBlockState(tablePos.offset(bookshelfOffset)).is(Blocks.BOOKSHELF) && !world.getBlockState(tablePos.offset(bookshelfOffset.getX() / 2, bookshelfOffset.getY(), bookshelfOffset.getZ() / 2)).isSuffocating(world, bookshelfOffset); ++ } // Purpur end + return world.getBlockState(tablePos.offset(bookshelfOffset)).is(Blocks.BOOKSHELF) && world.isEmptyBlock(tablePos.offset(bookshelfOffset.getX() / 2, bookshelfOffset.getY(), bookshelfOffset.getZ() / 2)); + } + +@@ -120,4 +126,18 @@ public class EnchantmentTableBlock extends BaseEntityBlock { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) { ++ BlockEntity blockEntity = level.getBlockEntity(pos); ++ ++ if (level.purpurConfig.enchantmentTableLapisPersists && blockEntity instanceof EnchantmentTableBlockEntity enchantmentTable) { ++ Containers.dropItemStack(level, pos.getX(), pos.getY(), pos.getZ(), new ItemStack(Items.LAPIS_LAZULI, enchantmentTable.getLapis())); ++ level.updateNeighbourForOutputSignal(pos, this); ++ } ++ ++ super.onRemove(state, level, pos, newState, moved); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +index 41d7cff39fc37955877668337689b4b26cd8c7cf..2deddc746e43896584bd65ba8e7971a80acb4a4d 100644 +--- a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +@@ -46,6 +46,14 @@ public class EndPortalBlock extends BaseEntityBlock { + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper + if (world instanceof ServerLevel && entity.canChangeDimensions() && Shapes.joinIsNotEmpty(Shapes.create(entity.getBoundingBox().move((double) (-pos.getX()), (double) (-pos.getY()), (double) (-pos.getZ()))), state.getShape(world, pos), BooleanOp.AND)) { ++ // Purpur start ++ if (entity.isPassenger() || entity.isVehicle()) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, PlayerTeleportEvent.TeleportCause.END_PORTAL).callEvent()) { ++ this.entityInside(state, world, pos, entity); ++ } ++ return; ++ } ++ // Purpur end + ResourceKey resourcekey = world.getTypeKey() == LevelStem.END ? Level.OVERWORLD : Level.END; // CraftBukkit - SPIGOT-6152: send back to main overworld in custom ends + ServerLevel worldserver = ((ServerLevel) world).getServer().getLevel(resourcekey); + +@@ -53,6 +61,22 @@ public class EndPortalBlock extends BaseEntityBlock { + // return; // CraftBukkit - always fire event in case plugins wish to change it + } + ++ // Purpur start ++ if (!world.purpurConfig.endPortalSafeTeleporting) { ++ // CraftBukkit start - Entity in portal ++ EntityPortalEnterEvent event = new EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ())); ++ world.getCraftServer().getPluginManager().callEvent(event); ++ ++ if (entity instanceof ServerPlayer) { ++ ((ServerPlayer) entity).changeDimension(worldserver, PlayerTeleportEvent.TeleportCause.END_PORTAL); ++ return; ++ } ++ // CraftBukkit end ++ entity.changeDimension(worldserver); ++ return; ++ } ++ // Purpur end ++ + // Paper start - move all of this logic into portal tick + entity.portalWorld = ((ServerLevel)world); + entity.portalBlock = pos.immutable(); +diff --git a/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java b/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java +index 7385e91f32f070e86a4e0fd3d214f55d832c7979..c3b78dd2d06be7d64920c6bcffcd16c82caa52b4 100644 +--- a/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EnderChestBlock.java +@@ -85,6 +85,27 @@ public class EnderChestBlock extends AbstractChestBlock i + EnderChestBlockEntity enderChestBlockEntity = (EnderChestBlockEntity)blockEntity; + playerEnderChestContainer.setActiveChest(enderChestBlockEntity); + player.openMenu(new SimpleMenuProvider((syncId, inventory, playerx) -> { ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.enderChestSixRows) { ++ if (org.purpurmc.purpur.PurpurConfig.enderChestPermissionRows) { ++ org.bukkit.craftbukkit.entity.CraftHumanEntity bukkitPlayer = player.getBukkitEntity(); ++ if (bukkitPlayer.hasPermission("purpur.enderchest.rows.six")) { ++ return ChestMenu.sixRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.five")) { ++ return ChestMenu.fiveRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.four")) { ++ return ChestMenu.fourRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.three")) { ++ return ChestMenu.threeRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.two")) { ++ return ChestMenu.twoRows(syncId, inventory, playerEnderChestContainer); ++ } else if (bukkitPlayer.hasPermission("purpur.enderchest.rows.one")) { ++ return ChestMenu.oneRow(syncId, inventory, playerEnderChestContainer); ++ } ++ } ++ return ChestMenu.sixRows(syncId, inventory, playerEnderChestContainer); ++ } ++ // Purpur end + return ChestMenu.threeRows(syncId, inventory, playerEnderChestContainer); + }, CONTAINER_TITLE)); + player.awardStat(Stats.OPEN_ENDERCHEST); +diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +index 34d744837e599633a3c2c0b72f253bb0e157f226..69cc276fecd4cac51d38bd3cc7de490ad0ae8ace 100644 +--- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +@@ -100,7 +100,7 @@ public class FarmBlock extends Block { + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { + super.fallOn(world, state, pos, entity, fallDistance); // CraftBukkit - moved here as game rules / events shouldn't affect fall damage. +- if (!world.isClientSide && world.random.nextFloat() < fallDistance - 0.5F && entity instanceof LivingEntity && (entity instanceof Player || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { ++ if (!world.isClientSide && (world.purpurConfig.farmlandTrampleHeight >= 0D ? fallDistance >= world.purpurConfig.farmlandTrampleHeight : world.random.nextFloat() < fallDistance - 0.5F) && entity instanceof LivingEntity && (entity instanceof Player || world.purpurConfig.farmlandBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { // Purpur + // CraftBukkit start - Interact soil + org.bukkit.event.Cancellable cancellable; + if (entity instanceof Player) { +@@ -114,6 +114,22 @@ public class FarmBlock extends Block { + return; + } + ++ // Purpur start ++ if (world.purpurConfig.farmlandTramplingDisabled) return; ++ if (world.purpurConfig.farmlandTramplingOnlyPlayers && !(entity instanceof Player)) return; ++ if (world.purpurConfig.farmlandAlpha) { ++ Block block = world.getBlockState(pos.below()).getBlock(); ++ if (block instanceof FenceBlock || block instanceof WallBlock) { ++ return; ++ } ++ } ++ if (world.purpurConfig.farmlandTramplingFeatherFalling) { ++ Iterator armor = entity.getArmorSlots().iterator(); ++ if (armor.hasNext() && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.FALL_PROTECTION, armor.next()) >= (int) entity.fallDistance) { ++ return; ++ } ++ } ++ // Purpur end + if (CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) { + return; + } +@@ -163,7 +179,7 @@ public class FarmBlock extends Block { + } + } + +- return false; ++ return ((ServerLevel) world).purpurConfig.farmlandGetsMoistFromBelow && world.getFluidState(pos.relative(Direction.DOWN)).is(FluidTags.WATER); // Purpur; + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +index 3a1aa4e2405090ccebefb7f5944f36462929e221..f3cf9f06de40054720d1847c1869a9d82592134d 100644 +--- a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +@@ -30,12 +30,12 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + + @Override + public BlockState getStateForPlacement(LevelAccessor world) { +- return (BlockState) this.defaultBlockState().setValue(GrowingPlantHeadBlock.AGE, world.getRandom().nextInt(25)); ++ return (BlockState) this.defaultBlockState().setValue(GrowingPlantHeadBlock.AGE, world.getRandom().nextInt(getMaxGrowthAge())); // Purpur + } + + @Override + public boolean isRandomlyTicking(BlockState state) { +- return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25; ++ return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) < getMaxGrowthAge(); // Purpur + } + + @Override +@@ -51,7 +51,7 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + } else { + modifier = world.spigotConfig.caveVinesModifier; + } +- if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25 && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution ++ if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < getMaxGrowthAge() && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution // Purpur + // Spigot end + BlockPos blockposition1 = pos.relative(this.growthDirection); + +@@ -73,11 +73,11 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + } + + public BlockState getMaxAgeState(BlockState state) { +- return (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, 25); ++ return (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, getMaxGrowthAge()); // Purpur + } + + public boolean isMaxAge(BlockState state) { +- return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) == 25; ++ return (Integer) state.getValue(GrowingPlantHeadBlock.AGE) >= getMaxGrowthAge(); // Purpur + } + + protected BlockState updateBodyAfterConvertedFromHead(BlockState from, BlockState to) { +@@ -119,13 +119,13 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + @Override + public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { + BlockPos blockposition1 = pos.relative(this.growthDirection); +- int i = Math.min((Integer) state.getValue(GrowingPlantHeadBlock.AGE) + 1, 25); ++ int i = Math.min((Integer) state.getValue(GrowingPlantHeadBlock.AGE) + 1, getMaxGrowthAge()); // Purpur + int j = this.getBlocksToGrowWhenBonemealed(random); + + for (int k = 0; k < j && this.canGrowInto(world.getBlockState(blockposition1)); ++k) { + world.setBlockAndUpdate(blockposition1, (BlockState) state.setValue(GrowingPlantHeadBlock.AGE, i)); + blockposition1 = blockposition1.relative(this.growthDirection); +- i = Math.min(i + 1, 25); ++ i = Math.min(i + 1, getMaxGrowthAge()); // Purpur + } + + } +@@ -138,4 +138,6 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + protected GrowingPlantHeadBlock getHeadBlock() { + return this; + } ++ ++ public abstract int getMaxGrowthAge(); // Purpur + } +diff --git a/src/main/java/net/minecraft/world/level/block/HayBlock.java b/src/main/java/net/minecraft/world/level/block/HayBlock.java +index cfbe1dae76db76cf54a4f5d72aca72d5e893859e..74cb10230d459ac9f300a9d59af504d233ac663e 100644 +--- a/src/main/java/net/minecraft/world/level/block/HayBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/HayBlock.java +@@ -15,6 +15,6 @@ public class HayBlock extends RotatedPillarBlock { + + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { +- entity.causeFallDamage(fallDistance, 0.2F, world.damageSources().fall()); ++ super.fallOn(world, state, pos, entity, fallDistance); // Purpur + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java b/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java +index 3c6d97b51c6fec130b80e5965afa2c49d48843c9..b456cb8efd8f0be8a6860c82462ce9bdde3a8383 100644 +--- a/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/HugeMushroomBlock.java +@@ -22,29 +22,65 @@ public class HugeMushroomBlock extends Block { + + public HugeMushroomBlock(BlockBehaviour.Properties settings) { + super(settings); +- this.registerDefaultState(this.stateDefinition.any().setValue(NORTH, Boolean.valueOf(true)).setValue(EAST, Boolean.valueOf(true)).setValue(SOUTH, Boolean.valueOf(true)).setValue(WEST, Boolean.valueOf(true)).setValue(UP, Boolean.valueOf(true)).setValue(DOWN, Boolean.valueOf(true))); ++ // Purpur start ++ this.registerDefaultState(this.stateDefinition.any() ++ .setValue(NORTH, true) ++ .setValue(EAST, true) ++ .setValue(SOUTH, true) ++ .setValue(WEST, true) ++ .setValue(UP, true) ++ .setValue(DOWN, true)); ++ // Purpur end + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return this.defaultBlockState(); // Purpur + BlockGetter blockGetter = ctx.getLevel(); + BlockPos blockPos = ctx.getClickedPos(); +- return this.defaultBlockState().setValue(DOWN, Boolean.valueOf(!blockGetter.getBlockState(blockPos.below()).is(this))).setValue(UP, Boolean.valueOf(!blockGetter.getBlockState(blockPos.above()).is(this))).setValue(NORTH, Boolean.valueOf(!blockGetter.getBlockState(blockPos.north()).is(this))).setValue(EAST, Boolean.valueOf(!blockGetter.getBlockState(blockPos.east()).is(this))).setValue(SOUTH, Boolean.valueOf(!blockGetter.getBlockState(blockPos.south()).is(this))).setValue(WEST, Boolean.valueOf(!blockGetter.getBlockState(blockPos.west()).is(this))); ++ // Purpur start ++ return this.defaultBlockState() ++ .setValue(DOWN, this != blockGetter.getBlockStateIfLoaded(blockPos.below()).getBlock()) ++ .setValue(UP, this != blockGetter.getBlockStateIfLoaded(blockPos.above()).getBlock()) ++ .setValue(NORTH, this != blockGetter.getBlockStateIfLoaded(blockPos.north()).getBlock()) ++ .setValue(EAST, this != blockGetter.getBlockStateIfLoaded(blockPos.east()).getBlock()) ++ .setValue(SOUTH, this != blockGetter.getBlockStateIfLoaded(blockPos.south()).getBlock()) ++ .setValue(WEST, this != blockGetter.getBlockStateIfLoaded(blockPos.west()).getBlock()); ++ // Purpur end + } + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return state; // Purpur + return neighborState.is(this) ? state.setValue(PROPERTY_BY_DIRECTION.get(direction), Boolean.valueOf(false)) : super.updateShape(state, direction, neighborState, world, pos, neighborPos); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { +- return state.setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.NORTH)), state.getValue(NORTH)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.SOUTH)), state.getValue(SOUTH)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.EAST)), state.getValue(EAST)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.WEST)), state.getValue(WEST)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.UP)), state.getValue(UP)).setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return state; ++ return state ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.NORTH)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.SOUTH)), state.getValue(SOUTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.EAST)), state.getValue(EAST)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.WEST)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.UP)), state.getValue(UP)) ++ .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur end + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { +- return state.setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.NORTH)), state.getValue(NORTH)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.SOUTH)), state.getValue(SOUTH)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.EAST)), state.getValue(EAST)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.WEST)), state.getValue(WEST)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.UP)), state.getValue(UP)).setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.disableMushroomBlockUpdates) return state; ++ return state ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.NORTH)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.SOUTH)), state.getValue(SOUTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.EAST)), state.getValue(EAST)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.WEST)), state.getValue(NORTH)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.UP)), state.getValue(UP)) ++ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.DOWN)), state.getValue(DOWN)); ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/IceBlock.java b/src/main/java/net/minecraft/world/level/block/IceBlock.java +index 5ecf02ce83b7496c977adfeb203b8eadb05f9da5..bf7f1ac5c691c0c4c30c124970f4b08a8108ad34 100644 +--- a/src/main/java/net/minecraft/world/level/block/IceBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/IceBlock.java +@@ -31,7 +31,7 @@ public class IceBlock extends HalfTransparentBlock { + public void afterDestroy(Level world, BlockPos pos, ItemStack tool) { + // Paper end + if (EnchantmentHelper.getItemEnchantmentLevel(Enchantments.SILK_TOUCH, tool) == 0) { +- if (world.dimensionType().ultraWarm()) { ++ if (world.isNether() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) { // Purpur + world.removeBlock(pos, false); + return; + } +@@ -59,7 +59,7 @@ public class IceBlock extends HalfTransparentBlock { + return; + } + // CraftBukkit end +- if (world.dimensionType().ultraWarm()) { ++ if (world.isNether() || (world.isTheEnd() && !org.purpurmc.purpur.PurpurConfig.allowWaterPlacementInTheEnd)) { // Purpur + world.removeBlock(pos, false); + } else { + world.setBlockAndUpdate(pos, Blocks.WATER.defaultBlockState()); +diff --git a/src/main/java/net/minecraft/world/level/block/KelpBlock.java b/src/main/java/net/minecraft/world/level/block/KelpBlock.java +index bc66fa91ec3e13431d5d9b6e17935cab73066be7..0f16b5ed2e249f3d8f583dc941e32066d354cf95 100644 +--- a/src/main/java/net/minecraft/world/level/block/KelpBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/KelpBlock.java +@@ -64,4 +64,11 @@ public class KelpBlock extends GrowingPlantHeadBlock implements LiquidBlockConta + public FluidState getFluidState(BlockState state) { + return Fluids.WATER.getSource(false); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.kelpMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java +index 43e8ef1d6a65d4fd3fe53a587639ffb814368217..9c22a730772f71b34c63d1e43d48943f71e9990b 100644 +--- a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java +@@ -105,7 +105,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + public void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { +- if (this.shouldSpreadLiquid(world, pos, state)) { ++ if (world.purpurConfig.tickFluids && this.shouldSpreadLiquid(world, pos, state)) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper + } + +@@ -129,7 +129,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { +- if (state.getFluidState().isSource() || neighborState.getFluidState().isSource()) { ++ if (world.getMinecraftWorld().purpurConfig.tickFluids && state.getFluidState().isSource() || neighborState.getFluidState().isSource()) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.fluid.getTickDelay(world)); + } + +@@ -138,7 +138,7 @@ public class LiquidBlock extends Block implements BucketPickup { + + @Override + public void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { +- if (this.shouldSpreadLiquid(world, pos, state)) { ++ if (world.purpurConfig.tickFluids && this.shouldSpreadLiquid(world, pos, state)) { // Purpur + world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper + } + +diff --git a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +index 12ffb5714f088f4aeafa1ad6a36f5b64a86c4c96..293aa5c8f91a997045f8d9f2951fe3a7f01f0642 100644 +--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +@@ -27,7 +27,7 @@ public class MagmaBlock extends Block { + + @Override + public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) { +- if (!entity.isSteppingCarefully() && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { ++ if ((!entity.isSteppingCarefully() || world.purpurConfig.magmaBlockDamageWhenSneaking) && entity instanceof LivingEntity && (world.purpurConfig.magmaBlockDamageWithFrostWalker || !EnchantmentHelper.hasFrostWalker((LivingEntity) entity))) { // Purpur + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit + entity.hurt(world.damageSources().hotFloor(), 1.0F); + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit +diff --git a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +index a6ab0d0defc05e56a91084c49897059670a1324b..589b437e7c97c846410f293e2f014bdcd7cb333e 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +@@ -52,7 +52,7 @@ public class NetherPortalBlock extends Block { + + @Override + public void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { +- if (world.spigotConfig.enableZombiePigmenPortalSpawns && world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(2000) < world.getDifficulty().getId()) { // Spigot ++ if (world.spigotConfig.enableZombiePigmenPortalSpawns && world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(world.purpurConfig.piglinPortalSpawnModifier) < world.getDifficulty().getId()) { // Spigot // Purpur + while (world.getBlockState(pos).is((Block) this)) { + pos = pos.below(); + } +@@ -84,6 +84,14 @@ public class NetherPortalBlock extends Block { + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper + if (entity.canChangeDimensions()) { ++ // Purpur start ++ if (entity.isPassenger() || entity.isVehicle()) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.NETHER_PORTAL).callEvent()) { ++ this.entityInside(state, world, pos, entity); ++ } ++ return; ++ } ++ // Purpur end + // CraftBukkit start - Entity in portal + EntityPortalEnterEvent event = new EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ())); + world.getCraftServer().getPluginManager().callEvent(event); +diff --git a/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java b/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java +index e55720c4d2fbdf6aae526910e87a67c29cf906fd..bf4485b4cad324d5aace657ebf284c4d97197f53 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherWartBlock.java +@@ -14,7 +14,7 @@ import net.minecraft.world.level.block.state.properties.IntegerProperty; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class NetherWartBlock extends BushBlock { ++public class NetherWartBlock extends BushBlock implements BonemealableBlock { // Purpur + + public static final int MAX_AGE = 3; + public static final IntegerProperty AGE = BlockStateProperties.AGE_3; +@@ -60,4 +60,32 @@ public class NetherWartBlock extends BushBlock { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(NetherWartBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public void playerDestroy(net.minecraft.world.level.Level world, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, @javax.annotation.Nullable net.minecraft.world.level.block.entity.BlockEntity blockEntity, ItemStack itemInHand) { ++ if (world.purpurConfig.hoeReplantsNetherWarts && itemInHand.getItem() instanceof net.minecraft.world.item.HoeItem) { ++ super.playerDestroyAndReplant(world, player, pos, state, blockEntity, itemInHand, Items.NETHER_WART); ++ } else { ++ super.playerDestroy(world, player, pos, state, blockEntity, itemInHand); ++ } ++ } ++ ++ @Override ++ public boolean isValidBonemealTarget(net.minecraft.world.level.LevelReader world, BlockPos pos, BlockState state, boolean isClient) { ++ return ((net.minecraft.world.level.Level) world).purpurConfig.netherWartAffectedByBonemeal && state.getValue(NetherWartBlock.AGE) < 3; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(net.minecraft.world.level.Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int i = Math.min(3, state.getValue(NetherWartBlock.AGE) + 1); ++ state = state.setValue(NetherWartBlock.AGE, i); ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, pos, state, 2); // CraftBukkit ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/NoteBlock.java b/src/main/java/net/minecraft/world/level/block/NoteBlock.java +index a9c2d254bda5686a35ad2393534b85030dd8b136..c11752564ea48960232844ee735779aa95d82c12 100644 +--- a/src/main/java/net/minecraft/world/level/block/NoteBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NoteBlock.java +@@ -62,11 +62,13 @@ public class NoteBlock extends Block { + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { ++ if (org.purpurmc.purpur.PurpurConfig.disableNoteBlockUpdates) return this.defaultBlockState(); // Purpur + return this.setInstrument(ctx.getLevel(), ctx.getClickedPos(), this.defaultBlockState()); + } + + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { ++ if (org.purpurmc.purpur.PurpurConfig.disableNoteBlockUpdates) return state; // Purpur + boolean flag = NoteBlock.isFeatureFlagEnabled(world) ? direction.getAxis() == Direction.Axis.Y : direction == Direction.DOWN; + + return flag ? this.setInstrument(world, pos, state) : super.updateShape(state, direction, neighborState, world, pos, neighborPos); +@@ -82,13 +84,14 @@ public class NoteBlock extends Block { + state = world.getBlockState(pos); // CraftBukkit - SPIGOT-5617: update in case changed in event + } + ++ if (!org.purpurmc.purpur.PurpurConfig.disableNoteBlockUpdates) // Purpur + world.setBlock(pos, (BlockState) state.setValue(NoteBlock.POWERED, flag1), 3); + } + + } + + private void playNote(@Nullable Entity entity, BlockState state, Level world, BlockPos pos) { +- if (!((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).requiresAirAbove() || world.getBlockState(pos.above()).isAir()) { ++ if (world.purpurConfig.noteBlockIgnoreAbove || !((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).requiresAirAbove() || world.getBlockState(pos.above()).isAir()) { // Purpur + // CraftBukkit start + // org.bukkit.event.block.NotePlayEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNotePlayEvent(world, pos, state.getValue(NoteBlock.INSTRUMENT), state.getValue(NoteBlock.NOTE)); + // if (event.isCancelled()) { +diff --git a/src/main/java/net/minecraft/world/level/block/ObserverBlock.java b/src/main/java/net/minecraft/world/level/block/ObserverBlock.java +index 7b45d6b9a005036ca5051d089a7be792eb87012f..8806c97ecc6bdd8a64c2d82bb2f58f46ac37c468 100644 +--- a/src/main/java/net/minecraft/world/level/block/ObserverBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ObserverBlock.java +@@ -64,6 +64,7 @@ public class ObserverBlock extends DirectionalBlock { + @Override + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(ObserverBlock.FACING) == direction && !(Boolean) state.getValue(ObserverBlock.POWERED)) { ++ if (!world.getMinecraftWorld().purpurConfig.disableObserverClocks || !(neighborState.getBlock() instanceof ObserverBlock) || neighborState.getValue(ObserverBlock.FACING).getOpposite() != direction) // Purpur + this.startSignal(world, pos); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +index 6b909d41ccdf6c1ac3ac0c4e673ff52f0d14a238..b8f69063cec4d31c9d9525a04c46ed8904ceff76 100644 +--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +@@ -188,7 +188,7 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + + @VisibleForTesting + public static void maybeTransferFluid(BlockState state, ServerLevel world, BlockPos pos, float dripChance) { +- if (dripChance <= 0.17578125F || dripChance <= 0.05859375F) { ++ if (dripChance <= world.purpurConfig.cauldronDripstoneWaterFillChance || dripChance <= world.purpurConfig.cauldronDripstoneLavaFillChance) { // Purpur + if (PointedDripstoneBlock.isStalactiteStartPos(state, world, pos)) { + Optional optional = PointedDripstoneBlock.getFluidAboveStalactite(world, pos, state); + +@@ -197,13 +197,13 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + float f1; + + if (fluidtype == Fluids.WATER) { +- f1 = 0.17578125F; ++ f1 = world.purpurConfig.cauldronDripstoneWaterFillChance; // Purpur + } else { + if (fluidtype != Fluids.LAVA) { + return; + } + +- f1 = 0.05859375F; ++ f1 = world.purpurConfig.cauldronDripstoneLavaFillChance; // Purpur + } + + if (dripChance < f1) { +diff --git a/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java b/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java +index 518d3832c36c9ecf1ed9267ffc1f926dc84b7989..af5933b886abf3fd17bfdb8c1cb1ea63f6f2a757 100644 +--- a/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PowderSnowBlock.java +@@ -72,7 +72,7 @@ public class PowderSnowBlock extends Block implements BucketPickup { + if (!world.isClientSide) { + // CraftBukkit start + if (entity.isOnFire() && entity.mayInteract(world, pos)) { +- if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !(world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || entity instanceof Player)).isCancelled()) { ++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !((world.purpurConfig.powderSnowBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) || entity instanceof Player)).isCancelled()) { + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +index 7fddb6fa8fd30ef88346a59f7867aae792f13772..40893e71fe8447b695350273bef9623bd5accdcd 100644 +--- a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +@@ -23,7 +23,7 @@ public class PoweredRailBlock extends BaseRailBlock { + } + + protected boolean findPoweredRailSignal(Level world, BlockPos pos, BlockState state, boolean flag, int distance) { +- if (distance >= 8) { ++ if (distance >= world.purpurConfig.railActivationRange) { // Purpur + return false; + } else { + int j = pos.getX(); +diff --git a/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java b/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java +index 2ed78cf83c0ae66a6ddba1ff307da89a24b0d0a8..ae17d6a54fad0bd2d71d306f418b5ced2f11b863 100644 +--- a/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RespawnAnchorBlock.java +@@ -141,7 +141,7 @@ public class RespawnAnchorBlock extends Block { + }; + Vec3 vec3d = explodedPos.getCenter(); + +- world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, explodedBlockState), explosiondamagecalculator, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // Paper ++ if (world.purpurConfig.respawnAnchorExplode)world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, explodedBlockState), explosiondamagecalculator, vec3d, (float) world.purpurConfig.respawnAnchorExplosionPower, world.purpurConfig.respawnAnchorExplosionFire, world.purpurConfig.respawnAnchorExplosionEffect); // Paper // Purpur + } + + public static boolean canSetSpawn(Level world) { +diff --git a/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java b/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java +index 437b44fb68bcbe81d1c431689431225b6a17a1a6..06d091b7c4df949c4abda16c4f73c194a71a4669 100644 +--- a/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SculkShriekerBlock.java +@@ -130,7 +130,7 @@ public class SculkShriekerBlock extends BaseEntityBlock implements SimpleWaterlo + @Nullable + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { +- return (BlockState) this.defaultBlockState().setValue(SculkShriekerBlock.WATERLOGGED, ctx.getLevel().getFluidState(ctx.getClickedPos()).getType() == Fluids.WATER); ++ return (BlockState) this.defaultBlockState().setValue(SculkShriekerBlock.WATERLOGGED, ctx.getLevel().getFluidState(ctx.getClickedPos()).getType() == Fluids.WATER).setValue(SculkShriekerBlock.CAN_SUMMON, ctx.getLevel().purpurConfig.sculkShriekerCanSummonDefault); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java +index c89978ecbc5a13dda6f76ea6d1cc3056efc9a174..39868ad3ee4bb573a4dd562894d93f64be4ee5ac 100644 +--- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java +@@ -138,7 +138,7 @@ public class ShulkerBoxBlock extends BaseEntityBlock { + public void playerWillDestroy(Level world, BlockPos pos, BlockState state, Player player) { + BlockEntity blockEntity = world.getBlockEntity(pos); + if (blockEntity instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity) { +- if (!world.isClientSide && player.isCreative() && !shulkerBoxBlockEntity.isEmpty()) { ++ if (world.purpurConfig.shulkerBoxAllowOversizedStacks || (!world.isClientSide && player.isCreative() && !shulkerBoxBlockEntity.isEmpty())) { // Purpur + ItemStack itemStack = getColoredItemStack(this.getColor()); + blockEntity.saveToItem(itemStack); + if (shulkerBoxBlockEntity.hasCustomName()) { +diff --git a/src/main/java/net/minecraft/world/level/block/SignBlock.java b/src/main/java/net/minecraft/world/level/block/SignBlock.java +index aface9a9697095a29edaf73c9cdabc2c1414b9d7..1a04d0a601b8e481dd6e2592b849b907a5b9f63f 100644 +--- a/src/main/java/net/minecraft/world/level/block/SignBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SignBlock.java +@@ -14,6 +14,7 @@ import net.minecraft.world.item.DyeItem; + import net.minecraft.world.item.Item; + import net.minecraft.world.item.ItemStack; + import net.minecraft.world.item.Items; ++import net.minecraft.world.item.SignItem; + import net.minecraft.world.level.BlockGetter; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.LevelAccessor; +@@ -76,11 +77,11 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo + if (world.isClientSide) { + return bl4 ? InteractionResult.SUCCESS : InteractionResult.CONSUME; + } else { +- BlockEntity bl5 = world.getBlockEntity(pos); +- if (!(bl5 instanceof SignBlockEntity)) { ++ BlockEntity blockEntity = world.getBlockEntity(pos); // Purpur - decompile fix ++ if (!(blockEntity instanceof SignBlockEntity)) { // Purpur - decompile fix + return InteractionResult.PASS; + } else { +- SignBlockEntity signBlockEntity = (SignBlockEntity)bl5; ++ SignBlockEntity signBlockEntity = (SignBlockEntity)blockEntity; // Purpur - decompile fix + boolean bl5 = signBlockEntity.hasGlowingText(); + if ((!bl2 || !bl5) && (!bl3 || bl5)) { + if (bl4) { +@@ -108,6 +109,17 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo + } + } + ++ // Purpur start - right click to open sign editor ++ if (world.purpurConfig.signRightClickEdit && itemStack.getItem() instanceof SignItem && ++ !player.isCrouching() && player.getAbilities().mayBuild && ++ player.getBukkitEntity().hasPermission("purpur.sign.edit")) { ++ signBlockEntity.setEditable(true); ++ signBlockEntity.setAllowedPlayerEditor(player.getUUID()); ++ player.openTextEdit(signBlockEntity); ++ return InteractionResult.SUCCESS; ++ } ++ // Purpur end ++ + return signBlockEntity.executeClickCommands((ServerPlayer)player) ? InteractionResult.SUCCESS : InteractionResult.PASS; + } else { + return InteractionResult.PASS; +diff --git a/src/main/java/net/minecraft/world/level/block/SlabBlock.java b/src/main/java/net/minecraft/world/level/block/SlabBlock.java +index 18b603d646081926343dea108b55d641df1c2c34..03ad3e45fc6d48091ac0c0ba5dc3d014b1d4ddfa 100644 +--- a/src/main/java/net/minecraft/world/level/block/SlabBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SlabBlock.java +@@ -130,4 +130,25 @@ public class SlabBlock extends Block implements SimpleWaterloggedBlock { + return false; + } + } ++ ++ // Purpur start ++ public boolean halfBreak(BlockState state, BlockPos pos, net.minecraft.server.level.ServerPlayer player) { ++ if (state.getValue(SlabBlock.TYPE) != SlabType.DOUBLE) { ++ return false; ++ } ++ net.minecraft.world.phys.HitResult result = player.getRayTrace(16, net.minecraft.world.level.ClipContext.Fluid.NONE); ++ if (result.getType() != net.minecraft.world.phys.HitResult.Type.BLOCK) { ++ return false; ++ } ++ double hitY = result.getLocation().y(); ++ int blockY = org.bukkit.util.NumberConversions.floor(hitY); ++ player.level.setBlock(pos, state.setValue(SlabBlock.TYPE, (hitY - blockY > 0.5 || blockY - pos.getY() == 1) ? SlabType.BOTTOM : SlabType.TOP), 3); ++ if (!player.getAbilities().instabuild) { ++ net.minecraft.world.entity.item.ItemEntity item = new net.minecraft.world.entity.item.ItemEntity(player.level, pos.getX(), pos.getY(), pos.getZ(), new ItemStack(asItem())); ++ item.setDefaultPickUpDelay(); ++ player.level.addFreshEntity(item); ++ } ++ return true; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java b/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java +index 14e00c7feb1c051d56a3d27cd00dcef072dd771a..4952fb1aaaafb55baa0fddb389f966a120a4786c 100644 +--- a/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SnowLayerBlock.java +@@ -81,6 +81,12 @@ public class SnowLayerBlock extends Block { + public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) { + BlockState iblockdata1 = world.getBlockState(pos.below()); + ++ // Purpur start ++ if (iblockdata1.is(Blocks.BLUE_ICE) && !world.getWorldBorder().world.purpurConfig.snowOnBlueIce) { ++ return false; ++ } ++ // Purpur end ++ + return iblockdata1.is(BlockTags.SNOW_LAYER_CANNOT_SURVIVE_ON) ? false : (iblockdata1.is(BlockTags.SNOW_LAYER_CAN_SURVIVE_ON) ? true : Block.isFaceFull(iblockdata1.getCollisionShape(world, pos.below()), Direction.UP) || iblockdata1.is((Block) this) && (Integer) iblockdata1.getValue(SnowLayerBlock.LAYERS) == 8); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java b/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java +index 936d844a5a246138c9f9ae4ae6e318242b8f1420..d58dc4aa02fe371deaf879df8692dbe93c648f9b 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpawnerBlock.java +@@ -40,6 +40,58 @@ public class SpawnerBlock extends BaseEntityBlock { + return createTickerHelper(type, BlockEntityType.MOB_SPAWNER, world.isClientSide ? SpawnerBlockEntity::clientTick : SpawnerBlockEntity::serverTick); + } + ++ // Purpur start ++ @Override ++ public void playerDestroy(Level level, net.minecraft.world.entity.player.Player player, BlockPos pos, BlockState state, BlockEntity blockEntity, ItemStack stack) { ++ if (level.purpurConfig.silkTouchEnabled && player.getBukkitEntity().hasPermission("purpur.drop.spawners") && isSilkTouch(level, stack)) { ++ Optional> type = net.minecraft.world.entity.EntityType.by(((SpawnerBlockEntity) blockEntity).getSpawner().nextSpawnData.getEntityToSpawn()); ++ ++ net.minecraft.world.entity.EntityType entityType = type.orElse(null); ++ final net.kyori.adventure.text.Component mobName = io.papermc.paper.adventure.PaperAdventure.asAdventure(entityType == null ? Component.empty() : entityType.getDescription()); ++ CompoundTag display = new CompoundTag(); ++ CompoundTag tag = new CompoundTag(); ++ ++ String name = level.purpurConfig.silkTouchSpawnerName; ++ if (name != null && !name.isEmpty() && !name.equals("Monster Spawner")) { ++ net.kyori.adventure.text.Component displayName = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name, net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("mob", mobName)); ++ if (name.startsWith("")) { ++ displayName = displayName.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ display.put("Name", net.minecraft.nbt.StringTag.valueOf(io.papermc.paper.adventure.PaperAdventure.asJsonString(displayName, java.util.Locale.ROOT))); ++ tag.put("display", display); ++ } ++ ++ List lore = level.purpurConfig.silkTouchSpawnerLore; ++ if (lore != null && !lore.isEmpty()) { ++ net.minecraft.nbt.ListTag list = new net.minecraft.nbt.ListTag(); ++ for (String line : lore) { ++ net.kyori.adventure.text.Component lineComponent = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(line, net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component("mob", mobName)); ++ if (line.startsWith("")) { ++ lineComponent = lineComponent.decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false); ++ } ++ list.add(net.minecraft.nbt.StringTag.valueOf(io.papermc.paper.adventure.PaperAdventure.asJsonString(lineComponent, java.util.Locale.ROOT))); ++ } ++ display.put("Lore", list); ++ tag.put("display", display); ++ } ++ ++ ItemStack item = new ItemStack(Blocks.SPAWNER.asItem()); ++ if (entityType != null) { ++ tag.putString("Purpur.mob_type", entityType.getName()); ++ tag.putDouble("HideFlags", 32); // hides the "Interact with Spawn Egg" tooltip ++ item.setTag(tag); ++ } ++ ++ popResource(level, pos, item); ++ } ++ super.playerDestroy(level, player, pos, state, blockEntity, stack); ++ } ++ ++ private boolean isSilkTouch(Level level, ItemStack stack) { ++ return stack != null && level.purpurConfig.silkTouchTools.contains(stack.getItem()) && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH, stack) >= level.purpurConfig.minimumSilkTouchSpawnerRequire; ++ } ++ // Purpur end ++ + @Override + public void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) { + super.spawnAfterBreak(state, world, pos, tool, dropExperience); +@@ -48,6 +100,7 @@ public class SpawnerBlock extends BaseEntityBlock { + + @Override + public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) { ++ if (isSilkTouch(worldserver, itemstack)) return 0; // Purpur + if (flag) { + int i = 15 + worldserver.random.nextInt(15) + worldserver.random.nextInt(15); + +diff --git a/src/main/java/net/minecraft/world/level/block/SpongeBlock.java b/src/main/java/net/minecraft/world/level/block/SpongeBlock.java +index 7304b2659eb45bc4bc9fa7c43e6ca07221d0fc73..df04a571ebd3c04bc7b58c1ee5661a1f03c69d2f 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpongeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpongeBlock.java +@@ -73,16 +73,16 @@ public class SpongeBlock extends Block { + // CraftBukkit end + Material material = iblockdata.getMaterial(); + +- if (fluid.is(FluidTags.WATER)) { ++ if (fluid.is(FluidTags.WATER) || (world.purpurConfig.spongeAbsorbsLava && fluid.is(FluidTags.LAVA))) { // Purpur + if (iblockdata.getBlock() instanceof BucketPickup && !((BucketPickup) iblockdata.getBlock()).pickupBlock(blockList, blockposition2, iblockdata).isEmpty()) { // CraftBukkit + ++i; +- if (j < 6) { ++ if (j < world.purpurConfig.spongeAbsorptionRadius) { // Purpur + queue.add(new Tuple<>(blockposition2, j + 1)); + } + } else if (iblockdata.getBlock() instanceof LiquidBlock) { + blockList.setBlock(blockposition2, Blocks.AIR.defaultBlockState(), 3); // CraftBukkit + ++i; +- if (j < 6) { ++ if (j < world.purpurConfig.spongeAbsorptionRadius) { // Purpur + queue.add(new Tuple<>(blockposition2, j + 1)); + } + } else if (material == Material.WATER_PLANT || material == Material.REPLACEABLE_WATER_PLANT) { +@@ -93,14 +93,14 @@ public class SpongeBlock extends Block { + blockList.setBlock(blockposition2, Blocks.AIR.defaultBlockState(), 3); + // CraftBukkit end + ++i; +- if (j < 6) { ++ if (j < world.purpurConfig.spongeAbsorptionRadius) { // Purpur + queue.add(new Tuple<>(blockposition2, j + 1)); + } + } + } + } + +- if (i > 64) { ++ if (i > world.purpurConfig.spongeAbsorptionArea) { // Purpur + break; + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java b/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java +index 0a95842c53a9d0286c57bcb42db97e468e30fb7d..e2d42e7947a237dd060ec1b9b63ac6ca4f37241a 100644 +--- a/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/StonecutterBlock.java +@@ -92,4 +92,16 @@ public class StonecutterBlock extends Block { + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return false; + } ++ ++ // Purpur start ++ @Override ++ public void stepOn(Level level, BlockPos pos, BlockState state, net.minecraft.world.entity.Entity entity) { ++ if (level.purpurConfig.stonecutterDamage > 0.0F && entity instanceof net.minecraft.world.entity.LivingEntity) { ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); ++ entity.hurt(entity.damageSources().magic(), level.purpurConfig.stonecutterDamage); ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; ++ } ++ super.stepOn(level, pos, state, entity); ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java +index 6b400a4759c8c8612a3b5c96ca0d87ef9dc71435..992de1ab2c00a2545a857f1b5533926bc895f996 100644 +--- a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java +@@ -19,7 +19,7 @@ import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class SugarCaneBlock extends Block { ++public class SugarCaneBlock extends Block implements BonemealableBlock { // Purpur + + public static final IntegerProperty AGE = BlockStateProperties.AGE_15; + protected static final float AABB_OFFSET = 6.0F; +@@ -106,4 +106,34 @@ public class SugarCaneBlock extends Block { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(SugarCaneBlock.AGE); + } ++ ++ // Purpur start ++ @Override ++ public boolean isValidBonemealTarget(LevelReader world, BlockPos pos, BlockState state, boolean isClient) { ++ if (!((net.minecraft.world.level.Level) world).purpurConfig.sugarCanAffectedByBonemeal || !world.isEmptyBlock(pos.above())) return false; ++ ++ int reedHeight = 0; ++ while (world.getBlockState(pos.below(reedHeight)).is(this)) { ++ reedHeight++; ++ } ++ ++ return reedHeight < ((net.minecraft.world.level.Level) world).paperConfig().maxGrowthHeight.reeds; ++ } ++ ++ @Override ++ public boolean isBonemealSuccess(net.minecraft.world.level.Level world, RandomSource random, BlockPos pos, BlockState state) { ++ return true; ++ } ++ ++ @Override ++ public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) { ++ int reedHeight = 0; ++ while (world.getBlockState(pos.below(reedHeight)).is(this)) { ++ reedHeight++; ++ } ++ for (int i = 0; i <= world.paperConfig().maxGrowthHeight.reeds - reedHeight; i++) { ++ world.setBlockAndUpdate(pos.above(i), state.setValue(SugarCaneBlock.AGE, 0)); ++ } ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java b/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java +index 6c1a0e6f961e46a1a89850746a71e97b32514adf..a8c227e2cb62cfa8225798329cde9078d194c776 100644 +--- a/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/TurtleEggBlock.java +@@ -160,7 +160,7 @@ public class TurtleEggBlock extends Block { + private boolean shouldUpdateHatchLevel(Level world) { + float f = world.getTimeOfDay(1.0F); + +- return (double) f < 0.69D && (double) f > 0.65D ? true : world.random.nextInt(500) == 0; ++ return (double) f < 0.69D && (double) f > 0.65D ? true : world.random.nextInt(world.purpurConfig.turtleEggsRandomTickCrackChance) == 0; + } + + @Override +@@ -193,6 +193,31 @@ public class TurtleEggBlock extends Block { + } + + private boolean canDestroyEgg(Level world, Entity entity) { +- return !(entity instanceof Turtle) && !(entity instanceof Bat) ? (!(entity instanceof LivingEntity) ? false : entity instanceof Player || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) : false; ++ // Purpur start ++ if (entity instanceof Turtle || entity instanceof Bat) { ++ return false; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromExpOrbs && entity instanceof net.minecraft.world.entity.ExperienceOrb) { ++ return true; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromItems && entity instanceof net.minecraft.world.entity.item.ItemEntity) { ++ return true; ++ } ++ if (world.purpurConfig.turtleEggsBreakFromMinecarts && entity instanceof net.minecraft.world.entity.vehicle.AbstractMinecart) { ++ return true; ++ } ++ if (!(entity instanceof LivingEntity)) { ++ return false; ++ } ++ if (world.purpurConfig.turtleEggsTramplingFeatherFalling) { ++ java.util.Iterator armor = entity.getArmorSlots().iterator(); ++ return !armor.hasNext() || net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(net.minecraft.world.item.enchantment.Enchantments.FALL_PROTECTION, armor.next()) < (int) entity.fallDistance; ++ } ++ if (entity instanceof Player) { ++ return true; ++ } ++ ++ return world.purpurConfig.turtleEggsBypassMobGriefing || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ // Purpur end + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java b/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java +index 6866605c7ef5361b21130a19a59c3fa3660dfb19..dee5d76d29da13f8639ab5d392cd0143201e71ba 100644 +--- a/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/TwistingVinesBlock.java +@@ -27,4 +27,11 @@ public class TwistingVinesBlock extends GrowingPlantHeadBlock { + protected boolean canGrowInto(BlockState state) { + return NetherVines.isValidGrowthState(state); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.twistingVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java b/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java +index e5c135ec059746b75fe58516809584221285cdbe..713c7e6e31a3e1097b612c77a4fce147c9252e0b 100644 +--- a/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WeepingVinesBlock.java +@@ -27,4 +27,11 @@ public class WeepingVinesBlock extends GrowingPlantHeadBlock { + protected boolean canGrowInto(BlockState state) { + return NetherVines.isValidGrowthState(state); + } ++ ++ // Purpur start ++ @Override ++ public int getMaxGrowthAge() { ++ return org.purpurmc.purpur.PurpurConfig.weepingVinesMaxGrowthAge; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +index b91effe91dad2e1aeea0ea31140f7432833b343f..bb628bd3fe8b185f356968697b17e1c4a442a6d2 100644 +--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -71,6 +71,7 @@ public class WitherSkullBlock extends SkullBlock { + entitywither.moveTo((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.55D, (double) blockposition1.getZ() + 0.5D, shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F, 0.0F); + entitywither.yBodyRot = shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F; + entitywither.makeInvulnerable(); ++ entitywither.setSummoner(iblockdata.getBlock().placer == null ? null : iblockdata.getBlock().placer.getUUID()); // Purpur + // CraftBukkit start + if (!world.addFreshEntity(entitywither, SpawnReason.BUILD_WITHER)) { + return; +diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index a3f073066f6e2eea8964461ad2b0409ade202f35..5cd7b4e7065070bf9fcc34b621dba2ccba99a573 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -44,6 +44,7 @@ import net.minecraft.world.level.Level; + import net.minecraft.world.level.block.AbstractFurnaceBlock; + import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.phys.Vec3; + // CraftBukkit start + import org.bukkit.craftbukkit.block.CraftBlock; +@@ -207,6 +208,22 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + // Paper end + } + ++ // Purpur start ++ public static void addFuel(ItemStack itemStack, Integer burnTime) { ++ Map map = Maps.newLinkedHashMap(); ++ map.putAll(getFuel()); ++ map.put(itemStack.getItem(), burnTime); ++ cachedBurnDurations = com.google.common.collect.ImmutableMap.copyOf(map); ++ } ++ ++ public static void removeFuel(ItemStack itemStack) { ++ Map map = Maps.newLinkedHashMap(); ++ map.putAll(getFuel()); ++ map.remove(itemStack.getItem()); ++ cachedBurnDurations = com.google.common.collect.ImmutableMap.copyOf(map); ++ } ++ // Purpur End ++ + // CraftBukkit start - add fields and methods + private int maxStack = MAX_STACK; + public List transaction = new java.util.ArrayList(); +@@ -324,6 +341,21 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + } + + ItemStack itemstack = (ItemStack) blockEntity.items.get(1); ++ // Purpur start ++ boolean usedLavaFromUnderneath = false; ++ if (world.purpurConfig.furnaceUseLavaFromUnderneath && !blockEntity.isLit() && itemstack.isEmpty() && !blockEntity.items.get(0).isEmpty() && world.getGameTime() % 20 == 0) { ++ BlockPos below = blockEntity.getBlockPos().below(); ++ BlockState belowState = world.getBlockStateIfLoaded(below); ++ if (belowState != null && belowState.is(Blocks.LAVA)) { ++ FluidState fluidState = belowState.getFluidState(); ++ if (fluidState != null && fluidState.isSource()) { ++ world.setBlock(below, Blocks.AIR.defaultBlockState(), 3); ++ itemstack = Items.LAVA_BUCKET.getDefaultInstance(); ++ usedLavaFromUnderneath = true; ++ } ++ } ++ } ++ // Purpur end + boolean flag2 = !((ItemStack) blockEntity.items.get(0)).isEmpty(); + boolean flag3 = !itemstack.isEmpty(); + +@@ -409,6 +441,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + setChanged(world, pos, state); + } + ++ if (usedLavaFromUnderneath) blockEntity.items.set(1, ItemStack.EMPTY); // Purpur + } + + private static boolean canBurn(RegistryAccess registryManager, @Nullable Recipe recipe, NonNullList slots, int count) { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +index 416aa989ebb18a8741cc9d605a1180ab830f6643..e38a0adf5463c48311ad08b8d2e5b5c2d989a3b5 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +@@ -67,7 +67,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + public BarrelBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.BARREL, pos, state); +- this.items = NonNullList.withSize(27, ItemStack.EMPTY); ++ // Purpur start ++ this.items = NonNullList.withSize(switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }, ItemStack.EMPTY); ++ // Purpur end + this.openersCounter = new ContainerOpenersCounter() { + @Override + protected void onOpen(Level world, BlockPos pos, BlockState state) { +@@ -118,7 +127,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + @Override + public int getContainerSize() { +- return 27; ++ // Purpur start ++ return switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }; ++ // Purpur end + } + + @Override +@@ -138,7 +156,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + + @Override + protected AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) { +- return ChestMenu.threeRows(syncId, playerInventory, this); ++ // Purpur start ++ return switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> ChestMenu.sixRows(syncId, playerInventory, this); ++ case 5 -> ChestMenu.fiveRows(syncId, playerInventory, this); ++ case 4 -> ChestMenu.fourRows(syncId, playerInventory, this); ++ case 2 -> ChestMenu.twoRows(syncId, playerInventory, this); ++ case 1 -> ChestMenu.oneRow(syncId, playerInventory, this); ++ default -> ChestMenu.threeRows(syncId, playerInventory, this); ++ }; ++ // Purpur end + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +index ef740d1ad6352ca4af299001a081b720bc472d2e..8f82b0ce87afc8890c5b3386d5f6e22c48974b16 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +@@ -84,6 +84,16 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + + public double getEffectRange() { + if (this.effectRange < 0) { ++ // Purpur Start ++ if (this.level != null) { ++ switch (this.levels) { ++ case 1: return this.level.purpurConfig.beaconLevelOne; ++ case 2: return this.level.purpurConfig.beaconLevelTwo; ++ case 3: return this.level.purpurConfig.beaconLevelThree; ++ case 4: return this.level.purpurConfig.beaconLevelFour; ++ } ++ } ++ // Purpur End + return this.levels * 10 + 10; + } else { + return effectRange; +@@ -155,6 +165,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + int j = pos.getY(); + int k = pos.getZ(); + BlockPos blockposition1; ++ boolean isTintedGlass = false; + + if (blockEntity.lastCheckY < j) { + blockposition1 = pos; +@@ -188,6 +199,9 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + } + } + } else { ++ if (world.purpurConfig.beaconAllowEffectsWithTintedGlass && block.equals(Blocks.TINTED_GLASS)) { ++ isTintedGlass = true; ++ } + if (tileentitybeacon_beaconcolortracker == null || iblockdata1.getLightBlock(world, blockposition1) >= 15 && !iblockdata1.is(Blocks.BEDROCK)) { + blockEntity.checkingBeamSections.clear(); + blockEntity.lastCheckY = l; +@@ -207,7 +221,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k); + } + +- if (blockEntity.levels > 0 && !blockEntity.beamSections.isEmpty()) { ++ if (blockEntity.levels > 0 && (!blockEntity.beamSections.isEmpty() || (world.purpurConfig.beaconAllowEffectsWithTintedGlass && isTintedGlass))) { + BeaconBlockEntity.applyEffects(world, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower, blockEntity); // Paper + BeaconBlockEntity.playSound(world, pos, SoundEvents.BEACON_AMBIENT); + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +index 41c9f074203915c31c1ae7a160ce509c13383f84..7b82842b97ce795745cf6ee6399f618c55acbbf3 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +@@ -43,7 +43,7 @@ public class BeehiveBlockEntity extends BlockEntity { + private final List stored = Lists.newArrayList(); + @Nullable + public BlockPos savedFlowerPos; +- public int maxBees = 3; // CraftBukkit - allow setting max amount of bees a hive can hold ++ public int maxBees = org.purpurmc.purpur.PurpurConfig.beeInsideBeeHive; // CraftBukkit - allow setting max amount of bees a hive can hold // Purpur + + public BeehiveBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.BEEHIVE, pos, state); +@@ -203,7 +203,7 @@ public class BeehiveBlockEntity extends BlockEntity { + } + + private static boolean releaseBee(Level world, BlockPos blockposition, BlockState iblockdata, BeehiveBlockEntity.BeeData tileentitybeehive_hivebee, @Nullable List list, BeehiveBlockEntity.BeeReleaseStatus tileentitybeehive_releasestatus, @Nullable BlockPos blockposition1, boolean force) { +- if (!force && (world.isNight() || world.isRaining()) && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) { ++ if (!force && ((world.isNight() && !world.purpurConfig.beeCanWorkAtNight) || (world.isRaining() && !world.purpurConfig.beeCanWorkInRain)) && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) { // Purpur + // CraftBukkit end + return false; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +index 1b248db497500aa6bd346b306dcb908af77626f3..e438e7e018f643d82ddf5efbf72779876c516d1a 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -6,6 +6,8 @@ import net.minecraft.CrashReportCategory; + import net.minecraft.core.BlockPos; + import net.minecraft.core.registries.BuiltInRegistries; + import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.StringTag; + import net.minecraft.network.protocol.Packet; + import net.minecraft.network.protocol.game.ClientGamePacketListener; + import net.minecraft.resources.ResourceLocation; +@@ -74,10 +76,27 @@ public abstract class BlockEntity { + if (persistentDataTag instanceof CompoundTag) { + this.persistentDataContainer.putAll((CompoundTag) persistentDataTag); + } ++ // Purpur start ++ if (nbt.contains("Purpur.persistentDisplayName")) { ++ this.persistentDisplayName = nbt.getString("Purpur.persistentDisplayName"); ++ } ++ if (nbt.contains("Purpur.persistentLore")) { ++ this.persistentLore = nbt.getList("Purpur.persistentLore", 8); ++ } ++ // Purpur end + } + // CraftBukkit end + +- protected void saveAdditional(CompoundTag nbt) {} ++ protected void saveAdditional(CompoundTag nbt) { ++ // Purpur start ++ if (this.persistentDisplayName != null) { ++ nbt.put("Purpur.persistentDisplayName", StringTag.valueOf(this.persistentDisplayName)); ++ } ++ if (this.persistentLore != null) { ++ nbt.put("Purpur.persistentLore", this.persistentLore); ++ } ++ // Purpur end ++ } + + public final CompoundTag saveWithFullMetadata() { + CompoundTag nbttagcompound = this.saveWithoutMetadata(); +@@ -187,10 +206,24 @@ public abstract class BlockEntity { + + @Nullable + public Packet getUpdatePacket() { ++ // Purpur start ++ if (this instanceof net.minecraft.world.Nameable nameable && nameable.hasCustomName()) { ++ CompoundTag nbt = this.saveWithoutMetadata(); ++ nbt.remove("Items"); ++ return net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket.create(this, $ -> nbt); ++ } ++ // Purpur end + return null; + } + + public CompoundTag getUpdateTag() { ++ // Purpur start ++ if (this instanceof net.minecraft.world.Nameable nameable && nameable.hasCustomName()) { ++ CompoundTag nbt = this.saveWithoutMetadata(); ++ nbt.remove("Items"); ++ return nbt; ++ } ++ // Purpur end + return new CompoundTag(); + } + +@@ -264,4 +297,24 @@ public abstract class BlockEntity { + } + // Paper end + ++ // Purpur start ++ private String persistentDisplayName = null; ++ private ListTag persistentLore = null; ++ ++ public void setPersistentDisplayName(String json) { ++ this.persistentDisplayName = json; ++ } ++ ++ public void setPersistentLore(ListTag lore) { ++ this.persistentLore = lore; ++ } ++ ++ public String getPersistentDisplayName() { ++ return this.persistentDisplayName; ++ } ++ ++ public ListTag getPersistentLore() { ++ return this.persistentLore; ++ } ++ // Purpur end + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +index 963a596154091b79ca139af6274aa323518ad1ad..4dcac3899a500d8586580bcfd5b4516e1dcdcd4a 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +@@ -171,7 +171,7 @@ public class ConduitBlockEntity extends BlockEntity { + if ((l > 1 || i1 > 1 || j1 > 1) && (i == 0 && (i1 == 2 || j1 == 2) || j == 0 && (l == 2 || j1 == 2) || k == 0 && (l == 2 || i1 == 2))) { + BlockPos blockposition2 = pos.offset(i, j, k); + BlockState iblockdata = world.getBlockState(blockposition2); +- Block[] ablock = ConduitBlockEntity.VALID_BLOCKS; ++ Block[] ablock = world.purpurConfig.conduitBlocks; // Purpur + int k1 = ablock.length; + + for (int l1 = 0; l1 < k1; ++l1) { +@@ -191,7 +191,7 @@ public class ConduitBlockEntity extends BlockEntity { + + private static void applyEffects(Level world, BlockPos pos, List activatingBlocks) { + int i = activatingBlocks.size(); +- int j = i / 7 * 16; ++ int j = i / 7 * world.purpurConfig.conduitDistance; // Purpur + int k = pos.getX(); + int l = pos.getY(); + int i1 = pos.getZ(); +@@ -222,21 +222,21 @@ public class ConduitBlockEntity extends BlockEntity { + blockEntity.destroyTarget = ConduitBlockEntity.findDestroyTarget(world, pos, blockEntity.destroyTargetUUID); + blockEntity.destroyTargetUUID = null; + } else if (blockEntity.destroyTarget == null) { +- List list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos), (entityliving1) -> { ++ List list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos, world), (entityliving1) -> { // Purpur + return entityliving1 instanceof Enemy && entityliving1.isInWaterOrRain(); + }); + + if (!list1.isEmpty()) { + blockEntity.destroyTarget = (LivingEntity) list1.get(world.random.nextInt(list1.size())); + } +- } else if (!blockEntity.destroyTarget.isAlive() || !pos.closerThan(blockEntity.destroyTarget.blockPosition(), 8.0D)) { ++ } else if (!blockEntity.destroyTarget.isAlive() || !pos.closerThan(blockEntity.destroyTarget.blockPosition(), world.purpurConfig.conduitDamageDistance)) { // Purpur + blockEntity.destroyTarget = null; + } + + if (blockEntity.destroyTarget != null) { + // CraftBukkit start + CraftEventFactory.blockDamage = CraftBlock.at(world, pos); +- if (blockEntity.destroyTarget.hurt(world.damageSources().magic(), 4.0F)) { ++ if (blockEntity.destroyTarget.hurt(world.damageSources().magic(), world.purpurConfig.conduitDamageAmount)) { // Purpur + world.playSound((Player) null, blockEntity.destroyTarget.getX(), blockEntity.destroyTarget.getY(), blockEntity.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F); + } + CraftEventFactory.blockDamage = null; +@@ -262,16 +262,22 @@ public class ConduitBlockEntity extends BlockEntity { + } + + private static AABB getDestroyRangeAABB(BlockPos pos) { ++ // Purpur start ++ return getDestroyRangeAABB(pos, null); ++ } ++ ++ private static AABB getDestroyRangeAABB(BlockPos pos, Level level) { ++ // Purpur end + int i = pos.getX(); + int j = pos.getY(); + int k = pos.getZ(); + +- return (new AABB((double) i, (double) j, (double) k, (double) (i + 1), (double) (j + 1), (double) (k + 1))).inflate(8.0D); ++ return (new AABB((double) i, (double) j, (double) k, (double) (i + 1), (double) (j + 1), (double) (k + 1))).inflate(level == null ? 8.0D : level.purpurConfig.conduitDamageDistance); // Purpur + } + + @Nullable + private static LivingEntity findDestroyTarget(Level world, BlockPos pos, UUID uuid) { +- List list = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos), (entityliving) -> { ++ List list = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos, world), (entityliving) -> { // Purpur + return entityliving.getUUID().equals(uuid); + }); + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java +index 65e1381bb2d10bd212463feb602c60f8fdb9ade1..b7370e64fd0d50e8725d7d5afc30af2e8bc8455d 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/EnchantmentTableBlockEntity.java +@@ -24,6 +24,7 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + public float tRot; + private static final RandomSource RANDOM = RandomSource.create(); + private Component name; ++ private int lapis = 0; // Purpur + + public EnchantmentTableBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.ENCHANTING_TABLE, pos, state); +@@ -35,6 +36,7 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + if (this.hasCustomName()) { + nbt.putString("CustomName", Component.Serializer.toJson(this.name)); + } ++ nbt.putInt("Purpur.Lapis", this.lapis); // Purpur + + } + +@@ -44,6 +46,7 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + if (nbt.contains("CustomName", 8)) { + this.name = io.papermc.paper.util.MCUtil.getBaseComponentFromNbt("CustomName", nbt); // Paper - Catch ParseException + } ++ this.lapis = nbt.getInt("Purpur.Lapis"); // Purpur + + } + +@@ -117,4 +120,14 @@ public class EnchantmentTableBlockEntity extends BlockEntity implements Nameable + public Component getCustomName() { + return this.name; + } ++ ++ // Purpur start ++ public int getLapis() { ++ return this.lapis; ++ } ++ ++ public void setLapis(int lapis) { ++ this.lapis = lapis; ++ } ++ // Purpur + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java +index 4da4edae517a0efec6e03a719ec47b700509dab1..9e760a8e8244b15daaf0abdfc5f8a51d5c663e12 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java +@@ -203,6 +203,23 @@ public class SignBlockEntity extends BlockEntity implements CommandSource { // C + return ClientboundBlockEntityDataPacket.create(this); + } + ++ // Purpur start ++ public ClientboundBlockEntityDataPacket getTranslatedUpdatePacket(boolean filtered) { ++ final CompoundTag nbt = new CompoundTag(); ++ this.saveAdditional(nbt); ++ final Component[] lines = getMessages(filtered); ++ for (int i = 0; i < 4; i++) { ++ final var component = io.papermc.paper.adventure.PaperAdventure.asAdventure(lines[i]); ++ final String line = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacyAmpersand().serialize(component); ++ final var text = net.kyori.adventure.text.Component.text(line); ++ final String json = net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().serialize(text); ++ nbt.putString("Text" + (i + 1), json); ++ } ++ nbt.putString("PurpurEditor", "true"); ++ return ClientboundBlockEntityDataPacket.create(this, entity -> nbt); ++ } ++ // Purpur end ++ + @Override + public CompoundTag getUpdateTag() { + return this.saveWithoutMetadata(); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +index c2bc747e9b9dd02971c13cb88e7aebeca8c0f2aa..a4cd9869988f414ce1bf14d38442f78207e3f048 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -178,6 +178,15 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + + public static void teleportEntity(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) { + if (world instanceof ServerLevel && !blockEntity.isCoolingDown()) { ++ if (!entity.canChangeDimensions()) return; // Purpur ++ // Purpur start ++ if (world.purpurConfig.imposeTeleportRestrictionsOnGateways && (entity.isVehicle() || entity.isPassenger())) { ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), entity.isPassenger() ? org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_PASSENGER : org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, PlayerTeleportEvent.TeleportCause.END_GATEWAY).callEvent()) { ++ teleportEntity(world, pos, state, entity, blockEntity); ++ } ++ return; ++ } ++ // Purpur end + ServerLevel worldserver = (ServerLevel) world; + + blockEntity.teleportCooldown = 100; +diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java b/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java +index 744d91546d1a810f60a43c15ed74b4158f341a4a..354538daefa603f6df5a139b6bff87dbb4cef178 100644 +--- a/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java ++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonStructureResolver.java +@@ -86,7 +86,7 @@ public class PistonStructureResolver { + return true; + } else { + int i = 1; +- if (i + this.toPush.size() > 12) { ++ if (i + this.toPush.size() > this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } else { + while(isSticky(blockState)) { +@@ -98,7 +98,7 @@ public class PistonStructureResolver { + } + + ++i; +- if (i + this.toPush.size() > 12) { ++ if (i + this.toPush.size() > this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } + } +@@ -142,7 +142,7 @@ public class PistonStructureResolver { + return true; + } + +- if (this.toPush.size() >= 12) { ++ if (this.toPush.size() >= this.level.purpurConfig.pistonBlockPushLimit) { // Purpur + return false; + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index 505503a3f59d4b747649275c6f6faa504b7c7b64..bee42ce7c1cb0f5ebd4890c02bc9c5dd727f7fd6 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -78,9 +78,9 @@ import net.minecraft.world.phys.shapes.VoxelShape; + public abstract class BlockBehaviour implements FeatureElement { + + protected static final Direction[] UPDATE_SHAPE_ORDER = new Direction[]{Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP}; +- protected final Material material; ++ public final Material material; // Purpur - protected -> public + public final boolean hasCollision; +- protected final float explosionResistance; ++ public float explosionResistance; // Purpur - protected final -> public + protected final boolean isRandomlyTicking; + protected final SoundType soundType; + protected final float friction; +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 13594b96cc8f451723c3598ef302ccee8e01bcac..e9d92bf484fb0d2fcb66a7c424eced109bfa031d 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -129,7 +129,7 @@ public class LevelChunk extends ChunkAccess { + this.blockTicks = blockTickScheduler; + this.fluidTicks = fluidTickScheduler; + +- this.lightningTick = this.level.getThreadUnsafeRandom().nextInt(100000) << 1; // Pufferfish - initialize lightning tick ++ this.lightningTick = java.util.concurrent.ThreadLocalRandom.current().nextInt(100000) << 1; // Pufferfish - initialize lightning tick // Purpur - any random will do + } + + // CraftBukkit start +@@ -925,7 +925,7 @@ public class LevelChunk extends ChunkAccess { + this.chunkHolder.getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system + + if (this.needsDecoration) { +- try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper ++ //try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper // Purpur + this.needsDecoration = false; + java.util.Random random = new java.util.Random(); + random.setSeed(this.level.getSeed()); +@@ -945,7 +945,7 @@ public class LevelChunk extends ChunkAccess { + } + } + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); +- } // Paper ++ //} // Paper // Purpur + } + } + } +@@ -1315,10 +1315,10 @@ public class LevelChunk extends ChunkAccess { + + if (LevelChunk.this.isTicking(blockposition)) { + try { +- ProfilerFiller gameprofilerfiller = LevelChunk.this.level.getProfiler(); ++ //ProfilerFiller gameprofilerfiller = LevelChunk.this.level.getProfiler(); // Purpur + +- gameprofilerfiller.push(this::getType); +- this.blockEntity.tickTimer.startTiming(); // Spigot ++ //gameprofilerfiller.push(this::getType); // Purpur ++ //this.blockEntity.tickTimer.startTiming(); // Spigot // Purpur + BlockState iblockdata = LevelChunk.this.getBlockState(blockposition); + + if (this.blockEntity.getType().isValid(iblockdata)) { +@@ -1329,7 +1329,7 @@ public class LevelChunk extends ChunkAccess { + LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata}); + } + +- gameprofilerfiller.pop(); ++ //gameprofilerfiller.pop(); // Purpur + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent tile entity and entity crashes +@@ -1340,7 +1340,7 @@ public class LevelChunk extends ChunkAccess { + // Paper end + // Spigot start + } finally { +- this.blockEntity.tickTimer.stopTiming(); ++ //this.blockEntity.tickTimer.stopTiming(); // Purpur + // Spigot end + } + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index 060e064625969610539dbf969ce773b877a7c579..32cd9df202704cdfb8fa06aaf0e738d483054feb 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -112,6 +112,7 @@ public class EntityStorage implements EntityPersistentStorage { + ListTag listTag = new ListTag(); + final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper + entities.forEach((entity) -> { // diff here: use entities parameter ++ if (!entity.canSaveToDisk()) return; // Purpur + // Paper start + final EntityType entityType = entity.getType(); + final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +index e8ae4449696d73c8c9b8b27d4d2e20db933a72cc..f55c50f6637a4f930b15565d6ac82bb4f27b9059 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -51,7 +51,7 @@ public class PhantomSpawner implements CustomSpawner { + int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; + this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; + // Paper end +- if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) { ++ if (world.getSkyDarken() < world.purpurConfig.phantomSpawnMinSkyDarkness && world.dimensionType().hasSkyLight()) { // Purpur + return 0; + } else { + int i = 0; +@@ -63,10 +63,10 @@ public class PhantomSpawner implements CustomSpawner { + if (!entityhuman.isSpectator() && (!world.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !entityhuman.isCreative())) { // Paper + BlockPos blockposition = entityhuman.blockPosition(); + +- if (!world.dimensionType().hasSkyLight() || blockposition.getY() >= world.getSeaLevel() && world.canSeeSky(blockposition)) { ++ if (!world.dimensionType().hasSkyLight() || (!world.purpurConfig.phantomSpawnOnlyAboveSeaLevel || blockposition.getY() >= world.getSeaLevel()) && (!world.purpurConfig.phantomSpawnOnlyWithVisibleSky || world.canSeeSky(blockposition))) { // Purpur + DifficultyInstance difficultydamagescaler = world.getCurrentDifficultyAt(blockposition); + +- if (difficultydamagescaler.isHarderThan(randomsource.nextFloat() * 3.0F)) { ++ if (difficultydamagescaler.isHarderThan(randomsource.nextFloat() * (float) world.purpurConfig.phantomSpawnLocalDifficultyChance)) { // Purpur + ServerStatsCounter serverstatisticmanager = ((ServerPlayer) entityhuman).getStats(); + int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE); + boolean flag2 = true; +@@ -78,7 +78,7 @@ public class PhantomSpawner implements CustomSpawner { + + if (NaturalSpawner.isValidEmptySpawnBlock(world, blockposition1, iblockdata, fluid, EntityType.PHANTOM)) { + SpawnGroupData groupdataentity = null; +- int k = 1 + randomsource.nextInt(difficultydamagescaler.getDifficulty().getId() + 1); ++ int k = world.purpurConfig.phantomSpawnMinPerAttempt + world.random.nextInt((world.purpurConfig.phantomSpawnMaxPerAttempt < 0 ? difficultydamagescaler.getDifficulty().getId() : world.purpurConfig.phantomSpawnMaxPerAttempt - world.purpurConfig.phantomSpawnMinPerAttempt) + 1); // Purpur + + for (int l = 0; l < k; ++l) { + // Paper start +diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +index 6063665b8848a2cd9f0b262eed36a9dd48db6035..5536b9e36b4ea99e2aaa62690b5bf00208291a58 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -226,7 +226,7 @@ public abstract class FlowingFluid extends Fluid { + } + } + +- if (this.canConvertToSource(world) && j >= 2) { ++ if (this.canConvertToSource(world) && j >= getRequiredSources(world)) { + BlockState iblockdata2 = world.getBlockState(pos.below()); + FluidState fluid1 = iblockdata2.getFluidState(); + +@@ -324,6 +324,12 @@ public abstract class FlowingFluid extends Fluid { + + protected abstract boolean canConvertToSource(Level world); + ++ // Purpur start ++ protected int getRequiredSources(Level level) { ++ return 2; ++ } ++ // Purpur end ++ + protected void spreadTo(LevelAccessor world, BlockPos pos, BlockState state, Direction direction, FluidState fluidState) { + if (state.getBlock() instanceof LiquidBlockContainer) { + ((LiquidBlockContainer) state.getBlock()).placeLiquid(world, pos, state, fluidState); +diff --git a/src/main/java/net/minecraft/world/level/material/LavaFluid.java b/src/main/java/net/minecraft/world/level/material/LavaFluid.java +index 783e315d92227cbcb5cd207b0a06a12e0778d14b..b05b4d3d97bca159c297f150005b5ab5bf6087e0 100644 +--- a/src/main/java/net/minecraft/world/level/material/LavaFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/LavaFluid.java +@@ -180,7 +180,7 @@ public abstract class LavaFluid extends FlowingFluid { + + @Override + public int getTickDelay(LevelReader world) { +- return world.dimensionType().ultraWarm() ? 10 : 30; ++ return world.dimensionType().ultraWarm() ? world.getWorldBorder().world.purpurConfig.lavaSpeedNether : world.getWorldBorder().world.purpurConfig.lavaSpeedNotNether; // Purpur + } + + @Override +@@ -198,6 +198,13 @@ public abstract class LavaFluid extends FlowingFluid { + world.levelEvent(1501, pos, 0); + } + ++ // Purpur start ++ @Override ++ protected int getRequiredSources(Level level) { ++ return level.purpurConfig.lavaInfiniteRequiredSources; ++ } ++ // Purpur end ++ + @Override + protected boolean canConvertToSource(Level world) { + return world.getGameRules().getBoolean(GameRules.RULE_LAVA_SOURCE_CONVERSION); +@@ -232,7 +239,7 @@ public abstract class LavaFluid extends FlowingFluid { + + @Override + protected float getExplosionResistance() { +- return 100.0F; ++ return Blocks.LAVA.getExplosionResistance(); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/material/WaterFluid.java b/src/main/java/net/minecraft/world/level/material/WaterFluid.java +index 82e85fbbd45244d02df90fa00c9046e7f51275a2..0f16deddd8cbb506ef7886f57ae640a42e841703 100644 +--- a/src/main/java/net/minecraft/world/level/material/WaterFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/WaterFluid.java +@@ -64,6 +64,13 @@ public abstract class WaterFluid extends FlowingFluid { + return world.getGameRules().getBoolean(GameRules.RULE_WATER_SOURCE_CONVERSION); + } + ++ // Purpur start ++ @Override ++ protected int getRequiredSources(Level level) { ++ return level.purpurConfig.waterInfiniteRequiredSources; ++ } ++ // Purpur end ++ + // Paper start + @Override + protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state, BlockPos source) { +@@ -109,7 +116,7 @@ public abstract class WaterFluid extends FlowingFluid { + + @Override + protected float getExplosionResistance() { +- return 100.0F; ++ return Blocks.WATER.getExplosionResistance(); // Purpur + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +index d23481453717f715124156b5d83f6448f720d049..a8af51a25b0f99c3a64d9150fdfcd6b818aa7581 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +@@ -53,8 +53,8 @@ public class PathFinder { + @Nullable + // Paper start - optimize collection + private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { +- profiler.push("find_path"); +- profiler.markForCharting(MetricCategory.PATH_FINDING); ++ //profiler.push("find_path"); // Purpur ++ //profiler.markForCharting(MetricCategory.PATH_FINDING); // Purpur + // Set set = positions.keySet(); + startNode.g = 0.0F; + startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +index 94a0fde36dda9404e5eb62d323c71dac1929a46b..a7578e112e80ed2585a7eb4fff9542f6068546be 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +@@ -243,7 +243,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + } + + if (blockPathTypes != BlockPathTypes.WALKABLE && (!this.isAmphibious() || blockPathTypes != BlockPathTypes.WATER)) { +- if ((node == null || node.costMalus < 0.0F) && maxYStep > 0 && (blockPathTypes != BlockPathTypes.FENCE || this.canWalkOverFences()) && blockPathTypes != BlockPathTypes.UNPASSABLE_RAIL && blockPathTypes != BlockPathTypes.TRAPDOOR && blockPathTypes != BlockPathTypes.POWDER_SNOW) { ++ if ((node == null || node.costMalus < 0.0F) && maxYStep > 0 && (blockPathTypes != BlockPathTypes.FENCE || this.canWalkOverFences()) && (this.mob.level.purpurConfig.mobsIgnoreRails || blockPathTypes != BlockPathTypes.UNPASSABLE_RAIL) && blockPathTypes != BlockPathTypes.TRAPDOOR && blockPathTypes != BlockPathTypes.POWDER_SNOW) { // Purpur + node = this.findAcceptedNode(x, y + 1, z, maxYStep - 1, prevFeetY, direction, nodeType); + if (node != null && (node.type == BlockPathTypes.OPEN || node.type == BlockPathTypes.WALKABLE) && this.mob.getBbWidth() < 1.0F) { + double g = (double)(x - direction.getStepX()) + 0.5D; +@@ -463,7 +463,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + return BlockPathTypes.BLOCKED; + } else { + // Paper end +- if (blockState.is(Blocks.CACTUS) || blockState.is(Blocks.SWEET_BERRY_BUSH)) { ++ if (blockState.is(Blocks.CACTUS) || blockState.is(Blocks.SWEET_BERRY_BUSH) || blockState.is(Blocks.STONECUTTER)) { // Purpur + return BlockPathTypes.DANGER_OTHER; + } + +@@ -493,7 +493,7 @@ public class WalkNodeEvaluator extends NodeEvaluator { + } else if (!blockState.is(BlockTags.TRAPDOORS) && !blockState.is(Blocks.LILY_PAD) && !blockState.is(Blocks.BIG_DRIPLEAF)) { + if (blockState.is(Blocks.POWDER_SNOW)) { + return BlockPathTypes.POWDER_SNOW; +- } else if (!blockState.is(Blocks.CACTUS) && !blockState.is(Blocks.SWEET_BERRY_BUSH)) { ++ } else if (!blockState.is(Blocks.CACTUS) && !blockState.is(Blocks.SWEET_BERRY_BUSH) && !blockState.is(Blocks.STONECUTTER)) { // Purpur + if (blockState.is(Blocks.HONEY_BLOCK)) { + return BlockPathTypes.STICKY_HONEY; + } else if (blockState.is(Blocks.COCOA)) { +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalShape.java b/src/main/java/net/minecraft/world/level/portal/PortalShape.java +index c461e0d04047db9c0c5ecc04063cebd38bf96ec2..e7554ec800f321e4e34c926c53f2375a8c3aa979 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalShape.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalShape.java +@@ -34,7 +34,7 @@ public class PortalShape { + private static final int MIN_HEIGHT = 3; + public static final int MAX_HEIGHT = 21; + private static final BlockBehaviour.StatePredicate FRAME = (iblockdata, iblockaccess, blockposition) -> { +- return iblockdata.is(Blocks.OBSIDIAN); ++ return iblockdata.is(Blocks.OBSIDIAN) || (org.purpurmc.purpur.PurpurConfig.cryingObsidianValidForPortalFrame && iblockdata.is(Blocks.CRYING_OBSIDIAN)); // Purpur + }; + private static final float SAFE_TRAVEL_MAX_ENTITY_XY = 4.0F; + private static final double SAFE_TRAVEL_MAX_VERTICAL_DELTA = 1.0D; +diff --git a/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java b/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java +index 31918fa2eb38e42a5ea5366e559f25ea9d7d59ae..15d8e9261a89da30529ac347462c520920ca4e7d 100644 +--- a/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java ++++ b/src/main/java/net/minecraft/world/level/storage/loot/functions/LootingEnchantFunction.java +@@ -49,6 +49,13 @@ public class LootingEnchantFunction extends LootItemConditionalFunction { + + if (entity instanceof LivingEntity) { + int i = EnchantmentHelper.getMobLooting((LivingEntity) entity); ++ // Purpur start ++ if (org.purpurmc.purpur.PurpurConfig.fixProjectileLootingTransfer && ++ context.getParamOrNull(LootContextParams.DIRECT_KILLER_ENTITY) ++ instanceof net.minecraft.world.entity.projectile.AbstractArrow arrow) { ++ i = arrow.lootingLevel; ++ } ++ // Purpur end + // CraftBukkit start - use lootingModifier if set by plugin + if (context.hasParam(LootContextParams.LOOTING_MOD)) { + i = context.getParamOrNull(LootContextParams.LOOTING_MOD); +diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java +index ffc76354ead6937daf366c3d87bcb51d3e4c47f5..5b98d42b5d6bc07265fbb017e51a6281c148436a 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -374,4 +374,10 @@ public class AABB { + public static AABB ofSize(Vec3 center, double dx, double dy, double dz) { + return new AABB(center.x - dx / 2.0D, center.y - dy / 2.0D, center.z - dz / 2.0D, center.x + dx / 2.0D, center.y + dy / 2.0D, center.z + dz / 2.0D); + } ++ ++ // Purpur - tuinity added method ++ public final AABB offsetY(double dy) { ++ return new AABB(this.minX, this.minY + dy, this.minZ, this.maxX, this.maxY + dy, this.maxZ); ++ } ++ // Purpur + } +diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +index 1d7c663fa0e550bd0cfb9a4b83ccd7e2968666f0..0043c0087896a6df6910b0500da37d84b287c901 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +@@ -86,20 +86,20 @@ public class LevelTicks implements LevelTickAccess { + } + + public void tick(long time, int maxTicks, BiConsumer ticker) { +- ProfilerFiller profilerFiller = this.profiler.get(); +- profilerFiller.push("collect"); +- this.collectTicks(time, maxTicks, profilerFiller); +- profilerFiller.popPush("run"); +- profilerFiller.incrementCounter("ticksToRun", this.toRunThisTick.size()); ++ //ProfilerFiller profilerFiller = this.profiler.get(); // Purpur ++ //profilerFiller.push("collect"); // Purpur ++ this.collectTicks(time, maxTicks, null); // Purpur ++ //profilerFiller.popPush("run"); // Purpur ++ //profilerFiller.incrementCounter("ticksToRun", this.toRunThisTick.size()); // Purpur + this.runCollectedTicks(ticker); +- profilerFiller.popPush("cleanup"); ++ //profilerFiller.popPush("cleanup"); // Purpur + this.cleanupAfterTick(); +- profilerFiller.pop(); ++ //profilerFiller.pop(); // Purpur + } + + private void collectTicks(long time, int maxTicks, ProfilerFiller profiler) { + this.sortContainersToTick(time); +- profiler.incrementCounter("containersToTick", this.containersToTick.size()); ++ //profiler.incrementCounter("containersToTick", this.containersToTick.size()); // Purpur + this.drainContainers(time, maxTicks); + this.rescheduleLeftoverContainers(); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +index 714afc98b5150907b45a00060be4e41582333204..312a6d90c0a09570aef24c205dc2ff277dcd4279 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +@@ -549,4 +549,213 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa + manager.save(); + } + } ++ ++ // Purpur start - OfflinePlayer API ++ @Override ++ public boolean getAllowFlight() { ++ if (this.isOnline()) { ++ return this.getPlayer().getAllowFlight(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ if (!data.contains("abilities")) return false; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getByte("mayfly") == (byte) 1; ++ } ++ } ++ ++ @Override ++ public void setAllowFlight(boolean flight) { ++ if (this.isOnline()) { ++ this.getPlayer().setAllowFlight(flight); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putByte("mayfly", (byte) (flight ? 1 : 0)); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public boolean isFlying() { ++ if (this.isOnline()) { ++ return this.isFlying(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ if (!data.contains("abilities")) return false; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getByte("flying") == (byte) 1; ++ } ++ } ++ ++ @Override ++ public void setFlying(boolean value) { ++ if (this.isOnline()) { ++ this.getPlayer().setFlying(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putByte("mayfly", (byte) (value ? 1 : 0)); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public void setFlySpeed(float value) throws IllegalArgumentException { ++ if (value < -1f || value > 1f) throw new IllegalArgumentException("FlySpeed needs to be between -1 and 1"); ++ if (this.isOnline()) { ++ this.getPlayer().setFlySpeed(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putFloat("flySpeed", value); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public float getFlySpeed() { ++ if (this.isOnline()) { ++ return this.getPlayer().getFlySpeed(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return 0; ++ if (!data.contains("abilities")) return 0; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getFloat("flySpeed"); ++ } ++ } ++ ++ @Override ++ public void setWalkSpeed(float value) throws IllegalArgumentException { ++ if (value < -1f || value > 1f) throw new IllegalArgumentException("WalkSpeed needs to be between -1 and 1"); ++ if (this.isOnline()) { ++ this.getPlayer().setWalkSpeed(value); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return; ++ if (!data.contains("abilities")) return; ++ CompoundTag abilities = data.getCompound("abilities"); ++ abilities.putFloat("walkSpeed", value); ++ data.put("abilities", abilities); ++ save(data); ++ } ++ } ++ ++ @Override ++ public float getWalkSpeed() { ++ if (this.isOnline()) { ++ return this.getPlayer().getWalkSpeed(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return 0; ++ if (!data.contains("abilities")) return 0; ++ CompoundTag abilities = data.getCompound("abilities"); ++ return abilities.getFloat("walkSpeed"); ++ } ++ } ++ ++ @Override ++ public Location getLocation() { ++ if (this.isOnline()) { ++ return this.getPlayer().getLocation(); ++ } else { ++ CompoundTag data = this.getData(); ++ if (data == null) return null; ++ long worldUUIDMost = data.getLong("WorldUUIDMost"); ++ long worldUUIDLeast = data.getLong("WorldUUIDLeast"); ++ net.minecraft.nbt.ListTag position = data.getList("Pos", org.bukkit.craftbukkit.util.CraftMagicNumbers.NBT.TAG_DOUBLE); ++ net.minecraft.nbt.ListTag rotation = data.getList("Rotation", org.bukkit.craftbukkit.util.CraftMagicNumbers.NBT.TAG_FLOAT); ++ UUID worldUuid = new UUID(worldUUIDMost, worldUUIDLeast); ++ org.bukkit.World world = server.getWorld(worldUuid); ++ double x = position.getDouble(0); ++ double y = position.getDouble(1); ++ double z = position.getDouble(2); ++ float yaw = rotation.getFloat(0); ++ float pitch = rotation.getFloat(1); ++ return new Location(world, x, y, z, yaw, pitch); ++ } ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleport(destination); ++ } else { ++ return setLocation(destination); ++ } ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause){ ++ if (this.isOnline()) { ++ return this.getPlayer().teleport(destination, cause); ++ } else { ++ return setLocation(destination); ++ } ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(Location destination) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleportAsync(destination); ++ } else { ++ return java.util.concurrent.CompletableFuture.completedFuture(setLocation(destination)); ++ } ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(Location destination, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) { ++ if (this.isOnline()) { ++ return this.getPlayer().teleportAsync(destination, cause); ++ } else { ++ return java.util.concurrent.CompletableFuture.completedFuture(setLocation(destination)); ++ } ++ } ++ ++ private boolean setLocation(Location location) { ++ CompoundTag data = this.getData(); ++ if (data == null) return false; ++ data.putLong("WorldUUIDMost", location.getWorld().getUID().getMostSignificantBits()); ++ data.putLong("WorldUUIDLeast", location.getWorld().getUID().getLeastSignificantBits()); ++ net.minecraft.nbt.ListTag position = new net.minecraft.nbt.ListTag(); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getX())); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getY())); ++ position.add(net.minecraft.nbt.DoubleTag.valueOf(location.getZ())); ++ data.put("Pos", position); ++ net.minecraft.nbt.ListTag rotation = new net.minecraft.nbt.ListTag(); ++ rotation.add(net.minecraft.nbt.FloatTag.valueOf(location.getYaw())); ++ rotation.add(net.minecraft.nbt.FloatTag.valueOf(location.getPitch())); ++ data.put("Rotation", rotation); ++ save(data); ++ return true; ++ } ++ ++ /** ++ * Safely replaces player's .dat file with provided CompoundTag ++ * @param compoundTag ++ */ ++ private void save(CompoundTag compoundTag) { ++ File playerDir = server.console.playerDataStorage.getPlayerDir(); ++ try { ++ File tempFile = File.createTempFile(this.getUniqueId()+"-", ".dat", playerDir); ++ net.minecraft.nbt.NbtIo.writeCompressed(compoundTag, tempFile); ++ File playerDataFile = new File(playerDir, this.getUniqueId()+".dat"); ++ File playerDataFileOld = new File(playerDir, this.getUniqueId()+".dat_old"); ++ net.minecraft.Util.safeReplaceFile(playerDataFile, tempFile, playerDataFileOld); ++ } catch (java.io.IOException e) { ++ e.printStackTrace(); ++ } ++ } ++ // Purpur end - OfflinePlayer API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index fa82680d6b8a28668b25b32bcbd34bddc9565c9c..e5bac6bc792196226f975e7f3dd8f147fb14dbad 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -317,6 +317,20 @@ public final class CraftServer implements Server { + this.structureManager = new CraftStructureManager(console.getStructureManager()); + + Bukkit.setServer(this); ++ // Purpur start ++ org.purpurmc.purpur.language.Language.setLanguage(new org.purpurmc.purpur.language.Language() { ++ private net.minecraft.locale.Language language = net.minecraft.locale.Language.getInstance(); ++ @Override ++ public boolean has(@org.jetbrains.annotations.NotNull String key) { ++ return language.has(key); ++ } ++ ++ @Override ++ public @org.jetbrains.annotations.NotNull String getOrDefault(@org.jetbrains.annotations.NotNull String key) { ++ return language.getOrDefault(key); ++ } ++ }); ++ // Purpur end + + // Register all the Enchantments and PotionTypes now so we can stop new registration immediately after + Enchantments.SHARPNESS.getClass(); +@@ -957,6 +971,7 @@ public final class CraftServer implements Server { + + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot + this.console.paperConfigurations.reloadConfigs(this.console); ++ org.purpurmc.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur + for (ServerLevel world : this.console.getAllLevels()) { + // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty + world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) +@@ -972,6 +987,7 @@ public final class CraftServer implements Server { + } + } + world.spigotConfig.init(); // Spigot ++ world.purpurConfig.init(); // Purpur + } + + Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper +@@ -987,6 +1003,7 @@ public final class CraftServer implements Server { + this.reloadData(); + org.spigotmc.SpigotConfig.registerCommands(); // Spigot + io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper ++ org.purpurmc.purpur.PurpurConfig.registerCommands(); // Purpur + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); + +@@ -1429,6 +1446,55 @@ public final class CraftServer implements Server { + return true; + } + ++ // Purpur Start ++ @Override ++ public void addFuel(org.bukkit.Material material, int burnTime) { ++ Preconditions.checkArgument(burnTime > 0, "BurnTime must be greater than 0"); ++ net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.addFuel(net.minecraft.world.item.ItemStack.fromBukkitCopy(new ItemStack(material)), burnTime); ++ } ++ ++ @Override ++ public void removeFuel(org.bukkit.Material material) { ++ net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.removeFuel(net.minecraft.world.item.ItemStack.fromBukkitCopy(new ItemStack(material))); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ this.worlds.forEach((name, world) -> world.sendBlockHighlight(location, duration, text, argb)); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ this.worlds.forEach((name, world) -> clearBlockHighlights()); ++ } ++ // Purpur End ++ + @Override + public List getRecipesFor(ItemStack result) { + Validate.notNull(result, "Result cannot be null"); +@@ -2700,6 +2766,7 @@ public final class CraftServer implements Server { + @Override + public double[] getTPS() { + return new double[] { ++ net.minecraft.server.MinecraftServer.getServer().tps5s.getAverage(), // Purpur + net.minecraft.server.MinecraftServer.getServer().tps1.getAverage(), + net.minecraft.server.MinecraftServer.getServer().tps5.getAverage(), + net.minecraft.server.MinecraftServer.getServer().tps15.getAverage() +@@ -2746,6 +2813,18 @@ public final class CraftServer implements Server { + return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console); + } + ++ // Purpur start ++ @Override ++ public YamlConfiguration getPurpurConfig() { ++ return org.purpurmc.purpur.PurpurConfig.config; ++ } ++ ++ @Override ++ public java.util.Properties getServerProperties() { ++ return getProperties().properties; ++ } ++ // Purpur end ++ + @Override + public void restart() { + org.spigotmc.RestartCommand.restart(); +@@ -2921,4 +3000,16 @@ public final class CraftServer implements Server { + } + + // Paper end ++ ++ // Purpur start ++ @Override ++ public String getServerName() { ++ return this.getProperties().serverName; ++ } ++ ++ @Override ++ public boolean isLagging() { ++ return getServer().lagging; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 8d3a32a0538a6065fd0725721ab8a1a011c4d64a..3013aeb442799aba5b2ae45edcb3c2c72a18a740 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2249,6 +2249,48 @@ public class CraftWorld extends CraftRegionAccessor implements World { + return (this.getHandle().dragonFight() == null) ? null : new CraftDragonBattle(this.getHandle().dragonFight()); + } + ++ // Purpur start ++ public float getLocalDifficultyAt(Location location) { ++ return getHandle().getCurrentDifficultyAt(io.papermc.paper.util.MCUtil.toBlockPosition(location)).getEffectiveDifficulty(); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ net.minecraft.network.protocol.game.DebugPackets.sendGameTestAddMarker(getHandle(), io.papermc.paper.util.MCUtil.toBlockPosition(location), text, argb, duration); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ net.minecraft.network.protocol.game.DebugPackets.sendGameTestClearPacket(getHandle()); ++ } ++ // Purpur end ++ + @Override + public PersistentDataContainer getPersistentDataContainer() { + return this.persistentDataContainer; +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index c103f10dbb6c06e14bb7b5df73a797f456803301..2a8db3527f3680789125fca41097657063efb32d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -173,6 +173,20 @@ public class Main { + .describedAs("Jar file"); + // Paper end + ++ // Purpur Start ++ acceptsAll(asList("purpur", "purpur-settings"), "File for purpur settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("purpur.yml")) ++ .describedAs("Yml file"); ++ ++ acceptsAll(asList("pufferfish", "pufferfish-settings"), "File for pufferfish settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("pufferfish.yml")) ++ .describedAs("Yml file"); ++ // Purpur end ++ + // Paper start + acceptsAll(asList("server-name"), "Name of the server") + .withRequiredArg() +@@ -277,7 +291,7 @@ public class Main { + System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper + } + +- if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { ++ if (false && Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { // Purpur + Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper + + Calendar deadline = Calendar.getInstance(); +diff --git a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java +index 3d0ce0803e1da8a2681a3cb41096ac942ece54a1..bcd075a771c7f43c6d1549aeec2ccb20ee168b57 100644 +--- a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java ++++ b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java +@@ -59,6 +59,7 @@ public class CraftEnchantment extends Enchantment { + return EnchantmentTarget.CROSSBOW; + case VANISHABLE: + return EnchantmentTarget.VANISHABLE; ++ case BOW_AND_CROSSBOW: return EnchantmentTarget.BOW_AND_CROSSBOW; // Purpur + default: + return null; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +index 75c7645fb5732c43d1da15181cf5c7ee4c3ecd6c..e7f5ea4d8d72672cf03483e720c6389425f28f6d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +@@ -27,12 +27,12 @@ public class CraftEndermite extends CraftMonster implements Endermite { + + @Override + public boolean isPlayerSpawned() { +- return false; ++ return getHandle().isPlayerSpawned(); // Purpur + } + + @Override + public void setPlayerSpawned(boolean playerSpawned) { +- // Nop ++ getHandle().setPlayerSpawned(playerSpawned); // Purpur + } + // Paper start + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 56c75029a94e8812c9e0ce5375aaa7cbcda90b87..97ea9612343e4288decd8daa9327a7e781877a8e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -209,6 +209,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + this.entity = entity; + } + ++ @Override ++ public boolean isInDaylight() { ++ return getHandle().isSunBurnTick(); ++ } ++ + public static CraftEntity getEntity(CraftServer server, Entity entity) { + /* + * Order is *EXTREMELY* important -- keep it right! =D +@@ -586,6 +591,10 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + // Paper end + + if ((!ignorePassengers && this.entity.isVehicle()) || this.entity.isRemoved()) { // Paper - Teleport passenger API ++ // Purpur start ++ if (!entity.isRemoved() && new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, cause).callEvent()) ++ return teleport(location, cause); ++ // Purpur end + return false; + } + +@@ -1442,4 +1451,37 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return !this.getHandle().level.noCollision(this.getHandle(), aabb); + } + // Paper End - Collision API ++ ++ // Purpur start ++ @Override ++ public org.bukkit.entity.Player getRider() { ++ net.minecraft.world.entity.player.Player rider = getHandle().getRider(); ++ return rider != null ? (org.bukkit.entity.Player) rider.getBukkitEntity() : null; ++ } ++ ++ @Override ++ public boolean hasRider() { ++ return getHandle().getRider() != null; ++ } ++ ++ @Override ++ public boolean isRidable() { ++ return getHandle().isRidable(); ++ } ++ ++ @Override ++ public boolean isRidableInWater() { ++ return !getHandle().dismountsUnderwater(); ++ } ++ ++ @Override ++ public boolean isImmuneToFire() { ++ return getHandle().fireImmune(); ++ } ++ ++ @Override ++ public void setImmuneToFire(Boolean fireImmune) { ++ getHandle().immuneToFire = fireImmune; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +index 1b008e5217c5bbf566a213abb92e1c7c43a3a7c2..468023414b4a9119a3418b8e8a5e38375bbd2407 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +@@ -266,6 +266,7 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { + @Override + public void recalculatePermissions() { + this.perm.recalculatePermissions(); ++ getHandle().canPortalInstant = hasPermission("purpur.portal.instant"); // Purpur + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +index 2966d4d466f44751b2f02afda2273a708c12b251..55f19324f92f98e497da49d3022e0edfc2351461 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +@@ -33,4 +33,17 @@ public class CraftIronGolem extends CraftGolem implements IronGolem { + public EntityType getType() { + return EntityType.IRON_GOLEM; + } ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +index a925b5c490e7129b27370aa57b5fad1cf05530c6..ea15690da167ec5e653da6f5afb55b33c45d1622 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +@@ -160,4 +160,51 @@ public class CraftItem extends CraftEntity implements Item { + public EntityType getType() { + return EntityType.DROPPED_ITEM; + } ++ ++ // Purpur start ++ @Override ++ public void setImmuneToCactus(boolean immuneToCactus) { ++ item.immuneToCactus = immuneToCactus; ++ } ++ ++ @Override ++ public boolean isImmuneToCactus() { ++ return item.immuneToCactus; ++ } ++ ++ @Override ++ public void setImmuneToExplosion(boolean immuneToExplosion) { ++ item.immuneToExplosion = immuneToExplosion; ++ } ++ ++ @Override ++ public boolean isImmuneToExplosion() { ++ return item.immuneToExplosion; ++ } ++ ++ @Override ++ public void setImmuneToFire(@org.jetbrains.annotations.Nullable Boolean immuneToFire) { ++ item.immuneToFire = (immuneToFire != null && immuneToFire); ++ } ++ ++ @Override ++ public void setImmuneToFire(boolean immuneToFire) { ++ this.setImmuneToFire((Boolean) immuneToFire); ++ } ++ ++ @Override ++ public boolean isImmuneToFire() { ++ return item.immuneToFire; ++ } ++ ++ @Override ++ public void setImmuneToLightning(boolean immuneToLightning) { ++ item.immuneToLightning = immuneToLightning; ++ } ++ ++ @Override ++ public boolean isImmuneToLightning() { ++ return item.immuneToLightning; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +index b548f06bc0569f0f1ee5edaa07806c3017d5399a..7ef5980f7321662aa7034a74c2f6926846425db9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +@@ -444,7 +444,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + net.minecraft.server.level.ServerPlayer entityPlayer = killer == null ? null : ((CraftPlayer) killer).getHandle(); + getHandle().lastHurtByPlayer = entityPlayer; + getHandle().lastHurtByMob = entityPlayer; +- getHandle().lastHurtByPlayerTime = entityPlayer == null ? 0 : 100; // 100 value taken from EntityLiving#damageEntity ++ getHandle().lastHurtByPlayerTime = entityPlayer == null ? 0 : getHandle().level.purpurConfig.mobLastHurtByPlayerTime; // 100 value taken from EntityLiving#damageEntity // Purpur + } + // Paper end + +@@ -456,7 +456,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + @Override + public boolean addPotionEffect(PotionEffect effect, boolean force) { + org.spigotmc.AsyncCatcher.catchOp("effect add"); // Paper +- this.getHandle().addEffect(new MobEffectInstance(MobEffect.byId(effect.getType().getId()), effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles(), effect.hasIcon()), EntityPotionEffectEvent.Cause.PLUGIN); // Paper - Don't ignore icon ++ this.getHandle().addEffect(new MobEffectInstance(MobEffect.byId(effect.getType().getId()), effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles(), effect.hasIcon(), effect.getKey()), EntityPotionEffectEvent.Cause.PLUGIN); // Purpur - add key // Paper - Don't ignore icon + return true; + } + +@@ -477,7 +477,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + @Override + public PotionEffect getPotionEffect(PotionEffectType type) { + MobEffectInstance handle = this.getHandle().getEffect(MobEffect.byId(type.getId())); +- return (handle == null) ? null : new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible()); ++ return (handle == null) ? null : new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible(), handle.getKey()); // Purpur - add key + } + + @Override +@@ -489,7 +489,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + public Collection getActivePotionEffects() { + List effects = new ArrayList(); + for (MobEffectInstance handle : this.getHandle().activeEffects.values()) { +- effects.add(new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible())); ++ effects.add(new PotionEffect(PotionEffectType.getById(MobEffect.getId(handle.getEffect())), handle.getDuration(), handle.getAmplifier(), handle.isAmbient(), handle.isVisible(), handle.getKey())); // Purpur - add key + } + return effects; + } +@@ -1071,4 +1071,32 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + getHandle().knockback(strength, directionX, directionZ); + }; + // Paper end ++ ++ // Purpur start ++ @Override ++ public float getSafeFallDistance() { ++ return getHandle().safeFallDistance; ++ } ++ ++ @Override ++ public void setSafeFallDistance(float safeFallDistance) { ++ getHandle().safeFallDistance = safeFallDistance; ++ } ++ ++ @Override ++ public void broadcastItemBreak(org.bukkit.inventory.EquipmentSlot slot) { ++ if (slot == null) return; ++ getHandle().broadcastBreakEvent(org.bukkit.craftbukkit.CraftEquipmentSlot.getNMS(slot)); ++ } ++ ++ @Override ++ public boolean shouldBurnInDay() { ++ return getHandle().shouldBurnInDay(); ++ } ++ ++ @Override ++ public void setShouldBurnInDay(boolean shouldBurnInDay) { ++ getHandle().setShouldBurnInDay(shouldBurnInDay); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +index 4d7a2c4c1001aefe9fcd4be8dbcb414f721bfff9..2c7716a9d65ebda209a144b82c2126b602aa9182 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +@@ -96,4 +96,16 @@ public class CraftLlama extends CraftChestedHorse implements Llama, com.destroys + return this.getHandle().caravanTail == null ? null : (Llama) this.getHandle().caravanTail.getBukkitEntity(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public boolean shouldJoinCaravan() { ++ return getHandle().shouldJoinCaravan; ++ } ++ ++ @Override ++ public void setShouldJoinCaravan(boolean shouldJoinCaravan) { ++ getHandle().shouldJoinCaravan = shouldJoinCaravan; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 3f498543cf0476ff1b184788d93f13b70c476c16..f4a341f72d727bbffa4cfcf72eda8ed0c8945c9e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -533,10 +533,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setPlayerListName(String name) { ++ // Purpur start ++ setPlayerListName(name, false); ++ } ++ public void setPlayerListName(String name, boolean useMM) { ++ // Purpur end + if (name == null) { + name = getName(); + } +- this.getHandle().listName = name.equals(getName()) ? null : CraftChatMessage.fromStringOrNull(name); ++ this.getHandle().listName = name.equals(getName()) ? null : useMM ? io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name)) : CraftChatMessage.fromStringOrNull(name); // Purpur + for (ServerPlayer player : (List) server.getHandle().players) { + if (player.getBukkitEntity().canSee(this)) { + player.connection.send(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, this.getHandle())); +@@ -1357,6 +1362,10 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + + if (entity.isVehicle() && !ignorePassengers) { // Paper - Teleport API ++ // Purpur start ++ if (new org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent(entity.getBukkitEntity(), org.purpurmc.purpur.event.entity.EntityTeleportHinderedEvent.Reason.IS_VEHICLE, cause).callEvent()) ++ return teleport(location, cause); ++ // Purpur end + return false; + } + +@@ -2404,6 +2413,28 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.getHandle().getAbilities().walkingSpeed * 2f; + } + ++ // Purpur start - OfflinePlayer API ++ @Override ++ public boolean teleportOffline(@NotNull Location destination) { ++ return this.teleport(destination); ++ } ++ ++ @Override ++ public boolean teleportOffline(Location destination, PlayerTeleportEvent.TeleportCause cause) { ++ return this.teleport(destination, cause); ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination) { ++ return this.teleportAsync(destination); ++ } ++ ++ @Override ++ public java.util.concurrent.CompletableFuture teleportOfflineAsync(@NotNull Location destination, PlayerTeleportEvent.TeleportCause cause) { ++ return this.teleportAsync(destination, cause); ++ } ++ // Purpur end - OfflinePlayer API ++ + private void validateSpeed(float value) { + if (value < 0) { + if (value < -1f) { +@@ -3196,4 +3227,97 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this.spigot; + } + // Spigot end ++ ++ // Purpur start ++ @Override ++ public boolean usesPurpurClient() { ++ return getHandle().purpurClient; ++ } ++ ++ @Override ++ public boolean isAfk() { ++ return getHandle().isAfk(); ++ } ++ ++ @Override ++ public void setAfk(boolean setAfk) { ++ getHandle().setAfk(setAfk); ++ } ++ ++ @Override ++ public void resetIdleTimer() { ++ getHandle().resetLastActionTime(); ++ } ++ ++ @Override ++ public boolean isSpawnInvulnerable() { ++ return getHandle().isSpawnInvulnerable(); ++ } ++ ++ @Override ++ public int getSpawnInvulnerableTicks() { ++ return getHandle().spawnInvulnerableTime; ++ } ++ ++ @Override ++ public void setSpawnInvulnerableTicks(int spawnInvulnerableTime) { ++ getHandle().spawnInvulnerableTime = spawnInvulnerableTime; ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration) { ++ sendBlockHighlight(location, duration, "", 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, int argb) { ++ sendBlockHighlight(location, duration, "", argb); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text) { ++ sendBlockHighlight(location, duration, text, 0x6400FF00); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, int argb) { ++ if (this.getHandle().connection == null) return; ++ FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); ++ buf.writeBlockPos(io.papermc.paper.util.MCUtil.toBlockPosition(location)); ++ buf.writeInt(argb); ++ buf.writeUtf(text); ++ buf.writeInt(duration); ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.DEBUG_GAME_TEST_ADD_MARKER, buf)); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, org.bukkit.Color color, int transparency) { ++ sendBlockHighlight(location, duration, "", color, transparency); ++ } ++ ++ @Override ++ public void sendBlockHighlight(Location location, int duration, String text, org.bukkit.Color color, int transparency) { ++ if (transparency < 0 || transparency > 255) throw new IllegalArgumentException("transparency is outside of 0-255 range"); ++ sendBlockHighlight(location, duration, text, transparency << 24 | color.asRGB()); ++ } ++ ++ @Override ++ public void clearBlockHighlights() { ++ if (this.getHandle().connection == null) return; ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.DEBUG_GAME_TEST_CLEAR, new FriendlyByteBuf(io.netty.buffer.Unpooled.buffer()))); ++ } ++ ++ @Override ++ public void sendDeathScreen(net.kyori.adventure.text.Component message) { ++ sendDeathScreen(message, null); ++ } ++ ++ @Override ++ public void sendDeathScreen(net.kyori.adventure.text.Component message, org.bukkit.entity.Entity killer) { ++ if (this.getHandle().connection == null) return; ++ net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket packet = new net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket(getEntityId(), killer == null ? -1 : killer.getEntityId(), null); ++ packet.adventure$message = message; ++ this.getHandle().connection.send(packet); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +index 42b7058d93fab8cbee49dba130734e1df9910096..5c6f55527cc0016f09b443528463b3906c433f8b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +@@ -34,4 +34,17 @@ public class CraftSnowman extends CraftGolem implements Snowman, com.destroystok + public EntityType getType() { + return EntityType.SNOWMAN; + } ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +index 4e880409b06086568627f3e930159f1abb979984..48fb7302b54f8e7f5c424210b550c03d4d071ea9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +@@ -223,4 +223,11 @@ public class CraftVillager extends CraftAbstractVillager implements Villager { + getHandle().getGossips().gossips.clear(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ public boolean isLobotomized() { ++ return getHandle().isLobotomized(); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +index 1a21d30620f13a48976da5ead7edab201ea68b21..a50a04dc2009515032058562627eba8e4406c5bb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +@@ -105,4 +105,17 @@ public class CraftWither extends CraftMonster implements Wither, com.destroystok + this.getHandle().makeInvulnerable(); + } + // Paper end ++ ++ // Purpur start ++ @Override ++ @org.jetbrains.annotations.Nullable ++ public java.util.UUID getSummoner() { ++ return getHandle().getSummoner(); ++ } ++ ++ @Override ++ public void setSummoner(@org.jetbrains.annotations.Nullable java.util.UUID summoner) { ++ getHandle().setSummoner(summoner); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +index e43fd3e59fd8c74828ae65965fade27f56beef65..b2f133c8baabba1cffa6e92ea0f854532f4c181b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +@@ -63,4 +63,16 @@ public class CraftWolf extends CraftTameableAnimal implements Wolf { + public void setInterested(boolean flag) { + this.getHandle().setIsInterested(flag); + } ++ ++ // Purpur start ++ @Override ++ public boolean isRabid() { ++ return getHandle().isRabid(); ++ } ++ ++ @Override ++ public void setRabid(boolean isRabid) { ++ getHandle().setRabid(isRabid); ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 221f5088953b3452966d07eabd4ea8b38c465fd9..bfb8039a65fbfdb6d2fa6fc4fdeb146fbc4e147f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -563,6 +563,15 @@ public class CraftEventFactory { + // Paper end + craftServer.getPluginManager().callEvent(event); + ++ // Purpur start ++ if (who != null) { ++ switch (action) { ++ case LEFT_CLICK_BLOCK, LEFT_CLICK_AIR -> who.processClick(InteractionHand.MAIN_HAND); ++ case RIGHT_CLICK_BLOCK, RIGHT_CLICK_AIR -> who.processClick(InteractionHand.OFF_HAND); ++ } ++ } ++ // Purpur end ++ + return event; + } + +@@ -1000,6 +1009,7 @@ public class CraftEventFactory { + damageCause = DamageCause.ENTITY_EXPLOSION; + } + event = new EntityDamageByEntityEvent(damager.getBukkitEntity(), entity.getBukkitEntity(), damageCause, modifiers, modifierFunctions, source.isCritical()); // Paper - add critical damage API ++ damager.processClick(InteractionHand.MAIN_HAND); // Purpur + } + event.setCancelled(cancelled); + +@@ -1114,6 +1124,7 @@ public class CraftEventFactory { + } else { + entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled + } ++ damager.getHandle().processClick(InteractionHand.MAIN_HAND); // Purpur + return event; + } + +@@ -1173,6 +1184,7 @@ public class CraftEventFactory { + EntityDamageEvent event; + if (damager != null) { + event = new EntityDamageByEntityEvent(damager.getBukkitEntity(), damagee.getBukkitEntity(), cause, modifiers, modifierFunctions, critical); // Paper - add critical damage API ++ damager.processClick(InteractionHand.MAIN_HAND); // Purpur + } else { + event = new EntityDamageEvent(damagee.getBukkitEntity(), cause, modifiers, modifierFunctions); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +index 04088918e172eecb8d53b0e6de9be0071ccf33b5..eddd6073e0feb7b046db1d169020ca067fdf689c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +@@ -182,8 +182,19 @@ public class CraftContainer extends AbstractContainerMenu { + case PLAYER: + case CHEST: + case ENDER_CHEST: ++ // Purpur start ++ this.delegate = new ChestMenu(org.purpurmc.purpur.PurpurConfig.enderChestSixRows ? MenuType.GENERIC_9x6 : MenuType.GENERIC_9x3, windowId, bottom, top, top.getContainerSize() / 9); ++ break; + case BARREL: +- this.delegate = new ChestMenu(MenuType.GENERIC_9x3, windowId, bottom, top, top.getContainerSize() / 9); ++ this.delegate = new ChestMenu(switch (org.purpurmc.purpur.PurpurConfig.barrelRows) { ++ case 6 -> MenuType.GENERIC_9x6; ++ case 5 -> MenuType.GENERIC_9x5; ++ case 4 -> MenuType.GENERIC_9x4; ++ case 2 -> MenuType.GENERIC_9x2; ++ case 1 -> MenuType.GENERIC_9x1; ++ default -> MenuType.GENERIC_9x3; ++ }, windowId, bottom, top, top.getContainerSize() / 9); ++ // Purpur end + break; + case DISPENSER: + case DROPPER: +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java +index 092f6843e3b43d4c615d2eee344f5966e96ae850..cb0c851ab5fcf676da2397040835a94d4bdb4be1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventory.java +@@ -83,7 +83,7 @@ public class CraftInventory implements Inventory { + + @Override + public void setContents(ItemStack[] items) { +- if (this.getSize() < items.length) { ++ if (false && this.getSize() < items.length) { // Purpur + throw new IllegalArgumentException("Invalid inventory size; expected " + this.getSize() + " or less"); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java +index 88d3ca586ff6905f18a8ab9f0e229f440ed44088..27dd4eb4781a3c75772860c11db886e1038cecd2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryAnvil.java +@@ -9,7 +9,7 @@ import org.bukkit.inventory.AnvilInventory; + public class CraftInventoryAnvil extends CraftResultInventory implements AnvilInventory { + + private final Location location; +- private final AnvilMenu container; ++ public final AnvilMenu container; // Purpur - private -> public + + public CraftInventoryAnvil(Location location, Container inventory, Container resultInventory, AnvilMenu container) { + super(inventory, resultInventory); +@@ -57,4 +57,26 @@ public class CraftInventoryAnvil extends CraftResultInventory implements AnvilIn + Preconditions.checkArgument(levels >= 0, "Maximum repair cost must be positive (or 0)"); + container.maximumRepairCost = levels; + } ++ ++ // Purpur start ++ @Override ++ public boolean canBypassCost() { ++ return container.bypassCost; ++ } ++ ++ @Override ++ public void setBypassCost(boolean bypassCost) { ++ container.bypassCost = bypassCost; ++ } ++ ++ @Override ++ public boolean canDoUnsafeEnchants() { ++ return container.canDoUnsafeEnchants; ++ } ++ ++ @Override ++ public void setDoUnsafeEnchants(boolean canDoUnsafeEnchants) { ++ container.canDoUnsafeEnchants = canDoUnsafeEnchants; ++ } ++ // Purpur end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java +index 0d4348ce1c4ec9bb779eaebf8606ea578f17d2cb..486768894f130cd23905cc7a8be16ce705667bbb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaPotion.java +@@ -13,6 +13,7 @@ import net.minecraft.nbt.ListTag; + import org.apache.commons.lang.Validate; + import org.bukkit.Color; + import org.bukkit.Material; ++import org.bukkit.NamespacedKey; + import org.bukkit.configuration.serialization.DelegateDeserialization; + import org.bukkit.craftbukkit.inventory.CraftMetaItem.SerializableMeta; + import org.bukkit.craftbukkit.potion.CraftPotionUtil; +@@ -42,6 +43,7 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + static final ItemMetaKey POTION_COLOR = new ItemMetaKey("CustomPotionColor", "custom-color"); + static final ItemMetaKey ID = new ItemMetaKey("Id", "potion-id"); + static final ItemMetaKey DEFAULT_POTION = new ItemMetaKey("Potion", "potion-type"); ++ static final ItemMetaKey KEY = new ItemMetaKey("Key", "namespacedkey"); // Purpur - add key + + // Having an initial "state" in ItemMeta seems bit dirty but the UNCRAFTABLE potion type + // is treated as the empty form of the meta because it represents an empty potion with no effect +@@ -92,7 +94,13 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + boolean ambient = effect.getBoolean(AMBIENT.NBT); + boolean particles = effect.contains(SHOW_PARTICLES.NBT, CraftMagicNumbers.NBT.TAG_BYTE) ? effect.getBoolean(SHOW_PARTICLES.NBT) : true; + boolean icon = effect.contains(SHOW_ICON.NBT, CraftMagicNumbers.NBT.TAG_BYTE) ? effect.getBoolean(SHOW_ICON.NBT) : particles; +- this.customEffects.add(new PotionEffect(type, duration, amp, ambient, particles, icon)); ++ // Purpur start ++ NamespacedKey key = null; ++ if (tag.contains(KEY.NBT)) { ++ key = NamespacedKey.fromString(effect.getString(KEY.NBT)); ++ } ++ this.customEffects.add(new PotionEffect(type, duration, amp, ambient, particles, icon, key)); ++ // Purpur end + } + } + } +@@ -141,6 +149,11 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + effectData.putBoolean(AMBIENT.NBT, effect.isAmbient()); + effectData.putBoolean(SHOW_PARTICLES.NBT, effect.hasParticles()); + effectData.putBoolean(SHOW_ICON.NBT, effect.hasIcon()); ++ // Purpur start ++ if (effect.hasKey()) { ++ effectData.putString(KEY.NBT, effect.getKey().toString()); ++ } ++ // Purpur end + effectList.add(effectData); + } + } +@@ -202,7 +215,7 @@ class CraftMetaPotion extends CraftMetaItem implements PotionMeta { + if (index != -1) { + if (overwrite) { + PotionEffect old = this.customEffects.get(index); +- if (old.getAmplifier() == effect.getAmplifier() && old.getDuration() == effect.getDuration() && old.isAmbient() == effect.isAmbient()) { ++ if (old.getAmplifier() == effect.getAmplifier() && old.getDuration() == effect.getDuration() && old.isAmbient() == effect.isAmbient() && old.getKey() == effect.getKey()) { // Purpur - add key + return false; + } + this.customEffects.set(index, effect); +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +index 8563fcf77eef0e1e354857b9a4256d78a523a8d0..b94c964790433cb7bd88ad16c5d82d1a8e39312d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +@@ -29,6 +29,7 @@ public interface CraftRecipe extends Recipe { + } else if (bukkit instanceof RecipeChoice.ExactChoice) { + stack = new Ingredient(((RecipeChoice.ExactChoice) bukkit).getChoices().stream().map((mat) -> new net.minecraft.world.item.crafting.Ingredient.ItemValue(CraftItemStack.asNMSCopy(mat)))); + stack.exact = true; ++ stack.predicate = ((RecipeChoice.ExactChoice) bukkit).getPredicate(); // Purpur + } else { + throw new IllegalArgumentException("Unknown recipe stack instance " + bukkit); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java +index 110503062b3043cffa082a1cda6b8d57152869aa..3e7e06bd5e9e4ed45c9e3452eb04e946fac817d8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java ++++ b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java +@@ -256,6 +256,7 @@ public final class CraftLegacy { + } + + static { ++ if (!org.purpurmc.purpur.PurpurConfig.loggerSuppressInitLegacyMaterialError) // Purpur + LOGGER.warn("Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug!"); // Paper - doesn't need to be an error + if (MinecraftServer.getServer() != null && MinecraftServer.getServer().isDebugging()) { + new Exception().printStackTrace(); +diff --git a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java +index acb69821a99aa69bce6d127e10976089c85be223..c5abd73981c5f4b41605eba0d44e6573dfd2a77a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java ++++ b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionUtil.java +@@ -101,7 +101,7 @@ public class CraftPotionUtil { + + public static MobEffectInstance fromBukkit(PotionEffect effect) { + MobEffect type = MobEffect.byId(effect.getType().getId()); +- return new MobEffectInstance(type, effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles()); ++ return new MobEffectInstance(type, effect.getDuration(), effect.getAmplifier(), effect.isAmbient(), effect.hasParticles(), effect.getKey()); // Purpur - add key + } + + public static PotionEffect toBukkit(MobEffectInstance effect) { +@@ -110,7 +110,7 @@ public class CraftPotionUtil { + int duration = effect.getDuration(); + boolean ambient = effect.isAmbient(); + boolean particles = effect.isVisible(); +- return new PotionEffect(type, duration, amp, ambient, particles); ++ return new PotionEffect(type, duration, amp, ambient, particles, effect.getKey()); // Purpur - add key + } + + public static boolean equals(MobEffect mobEffect, PotionEffectType type) { +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index d1526ed7197b883e1d1f07baf285bf5eef4d20d5..5402098dce0d64d3dceea51f248d7d366850a74f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -516,10 +516,10 @@ public class CraftScheduler implements BukkitScheduler { + this.runners.remove(task.getTaskId()); + } + } +- MinecraftTimings.bukkitSchedulerFinishTimer.startTiming(); // Paper ++ //MinecraftTimings.bukkitSchedulerFinishTimer.startTiming(); // Paper // Purpur + this.pending.addAll(temp); + temp.clear(); +- MinecraftTimings.bukkitSchedulerFinishTimer.stopTiming(); // Paper ++ //MinecraftTimings.bukkitSchedulerFinishTimer.stopTiming(); // Paper // Purpur + //this.debugHead = this.debugHead.getNextHead(currentTick); // Paper + } + +@@ -563,7 +563,7 @@ public class CraftScheduler implements BukkitScheduler { + } + + void parsePending() { // Paper +- if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.startTiming(); // Paper ++ //if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.startTiming(); // Paper // Purpur + CraftTask head = this.head; + CraftTask task = head.getNext(); + CraftTask lastTask = head; +@@ -582,7 +582,7 @@ public class CraftScheduler implements BukkitScheduler { + task.setNext(null); + } + this.head = lastTask; +- if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.stopTiming(); // Paper ++ //if (!this.isAsyncScheduler) MinecraftTimings.bukkitSchedulerPendingTimer.stopTiming(); // Paper // Purpur + } + + private boolean isReady(final int currentTick) { +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +index 3f45bab0e9f7b3697e6d9d1092a1e6e579f7066f..4f1cf281c4bf68c37982d390da8779dea78dab18 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +@@ -96,13 +96,13 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot + + @Override + public void run() { +- try (Timing ignored = timings.startTiming()) { // Paper ++ //try (Timing ignored = timings.startTiming()) { // Paper // Purpur + if (this.rTask != null) { + this.rTask.run(); + } else { + this.cTask.accept(this); + } +- } // Paper ++ //} // Paper // Purpur + } + + long getCreatedAt() { +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +index 138407c2d4b0bc55ddb9aac5d2aa3edadda090fb..a6e9e503a496c18e2501b03ec84f4600c134a50c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +@@ -115,7 +115,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + public void getScoreboardScores(ObjectiveCriteria criteria, String name, Consumer consumer) { + // Paper start - add timings for scoreboard search + // plugins leaking scoreboards will make this very expensive, let server owners debug it easily +- co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); ++ //co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); // Purpur + try { + // Paper end - add timings for scoreboard search + for (CraftScoreboard scoreboard : this.scoreboards) { +@@ -123,7 +123,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + board.forAllObjectives(criteria, name, (score) -> consumer.accept(score)); + } + } finally { // Paper start - add timings for scoreboard search +- co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); ++ //co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); // Purpur + } + // Paper end - add timings for scoreboard search + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +index ec771c480156db393c326fa2fbdc2d432fb76f53..71940bf3a4162d12a422a5b3100ad8def85f95ac 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +@@ -23,7 +23,15 @@ public final class CommandPermissions { + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "kick", "Allows the user to kick players", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "stop", "Allows the user to stop the server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "list", "Allows the user to list all online players", PermissionDefault.OP, commands); +- DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "gamemode", "Allows the user to change the gamemode of another player", PermissionDefault.OP, commands); ++ // Purpur start ++ Permission gamemodeVanilla = DefaultPermissions.registerPermission(PREFIX + "gamemode", "Allows the user to change the gamemode", PermissionDefault.OP, commands); ++ for (net.minecraft.world.level.GameType gametype : net.minecraft.world.level.GameType.values()) { ++ Permission gamemodeSelf = DefaultPermissions.registerPermission(PREFIX + "gamemode." + gametype.getName(), "Allows the user to set " + gametype.getName() + " gamemode for self", PermissionDefault.OP); ++ Permission gamemodeOther = DefaultPermissions.registerPermission(PREFIX + "gamemode." + gametype.getName() + ".other", "Allows the user to set " + gametype.getName() + " gamemode for other players", PermissionDefault.OP); ++ gamemodeSelf.addParent(gamemodeOther, true); ++ gamemodeVanilla.addParent(gamemodeSelf, true); ++ } ++ // Purpur end + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "xp", "Allows the user to give themselves or others arbitrary values of experience", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "defaultgamemode", "Allows the user to change the default gamemode of the server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "seed", "Allows the user to view the seed of the world", PermissionDefault.OP, commands); +diff --git a/src/main/java/org/purpurmc/purpur/PurpurConfig.java b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..62ad4c6ad417e11e9152f74636b2ff0d187d0799 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +@@ -0,0 +1,636 @@ ++package org.purpurmc.purpur; ++ ++import com.google.common.base.Throwables; ++import com.google.common.collect.ImmutableMap; ++import com.mojang.datafixers.util.Pair; ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.effect.MobEffect; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.entity.EntityDimensions; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.food.FoodProperties; ++import net.minecraft.world.food.Foods; ++import net.minecraft.world.item.enchantment.Enchantment; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import org.bukkit.Bukkit; ++import org.bukkit.command.Command; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.InvalidConfigurationException; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.purpurmc.purpur.command.PurpurCommand; ++import org.purpurmc.purpur.task.TPSBarTask; ++ ++import java.io.File; ++import java.io.IOException; ++import java.lang.reflect.InvocationTargetException; ++import java.lang.reflect.Method; ++import java.lang.reflect.Modifier; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.logging.Level; ++ ++@SuppressWarnings("unused") ++public class PurpurConfig { ++ private static final String HEADER = "This is the main configuration file for Purpur.\n" ++ + "As you can see, there's tons to configure. Some options may impact gameplay, so use\n" ++ + "with caution, and make sure you know what each option does before configuring.\n" ++ + "\n" ++ + "If you need help with the configuration or have any questions related to Purpur,\n" ++ + "join us in our Discord guild.\n" ++ + "\n" ++ + "Website: https://purpurmc.org \n" ++ + "Docs: https://purpurmc.org/docs \n"; ++ private static File CONFIG_FILE; ++ public static YamlConfiguration config; ++ ++ private static Map commands; ++ ++ public static int version; ++ static boolean verbose; ++ ++ public static void init(File configFile) { ++ CONFIG_FILE = configFile; ++ config = new YamlConfiguration(); ++ try { ++ config.load(CONFIG_FILE); ++ } catch (IOException ignore) { ++ } catch (InvalidConfigurationException ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Could not load purpur.yml, please correct your syntax errors", ex); ++ throw Throwables.propagate(ex); ++ } ++ config.options().header(HEADER); ++ config.options().copyDefaults(true); ++ verbose = getBoolean("verbose", false); ++ ++ commands = new HashMap<>(); ++ commands.put("purpur", new PurpurCommand("purpur")); ++ ++ version = getInt("config-version", 32); ++ set("config-version", 32); ++ ++ readConfig(PurpurConfig.class, null); ++ ++ Blocks.rebuildCache(); ++ } ++ ++ protected static void log(String s) { ++ if (verbose) { ++ log(Level.INFO, s); ++ } ++ } ++ ++ protected static void log(Level level, String s) { ++ Bukkit.getLogger().log(level, s); ++ } ++ ++ public static void registerCommands() { ++ for (Map.Entry entry : commands.entrySet()) { ++ MinecraftServer.getServer().server.getCommandMap().register(entry.getKey(), "Purpur", entry.getValue()); ++ } ++ } ++ ++ static void readConfig(Class clazz, Object instance) { ++ for (Method method : clazz.getDeclaredMethods()) { ++ if (Modifier.isPrivate(method.getModifiers())) { ++ if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) { ++ try { ++ method.setAccessible(true); ++ method.invoke(instance); ++ } catch (InvocationTargetException ex) { ++ throw Throwables.propagate(ex.getCause()); ++ } catch (Exception ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Error invoking " + method, ex); ++ } ++ } ++ } ++ } ++ ++ try { ++ config.save(CONFIG_FILE); ++ } catch (IOException ex) { ++ Bukkit.getLogger().log(Level.SEVERE, "Could not save " + CONFIG_FILE, ex); ++ } ++ } ++ ++ private static void set(String path, Object val) { ++ config.addDefault(path, val); ++ config.set(path, val); ++ } ++ ++ private static String getString(String path, String def) { ++ config.addDefault(path, def); ++ return config.getString(path, config.getString(path)); ++ } ++ ++ private static boolean getBoolean(String path, boolean def) { ++ config.addDefault(path, def); ++ return config.getBoolean(path, config.getBoolean(path)); ++ } ++ ++ private static double getDouble(String path, double def) { ++ config.addDefault(path, def); ++ return config.getDouble(path, config.getDouble(path)); ++ } ++ ++ private static int getInt(String path, int def) { ++ config.addDefault(path, def); ++ return config.getInt(path, config.getInt(path)); ++ } ++ ++ private static List getList(String path, T def) { ++ config.addDefault(path, def); ++ return config.getList(path, config.getList(path)); ++ } ++ ++ static Map getMap(String path, Map def) { ++ if (def != null && config.getConfigurationSection(path) == null) { ++ config.addDefault(path, def); ++ return def; ++ } ++ return toMap(config.getConfigurationSection(path)); ++ } ++ ++ private static Map toMap(ConfigurationSection section) { ++ ImmutableMap.Builder builder = ImmutableMap.builder(); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ Object obj = section.get(key); ++ if (obj != null) { ++ builder.put(key, obj instanceof ConfigurationSection val ? toMap(val) : obj); ++ } ++ } ++ } ++ return builder.build(); ++ } ++ ++ public static String cannotRideMob = "You cannot mount that mob"; ++ public static String afkBroadcastAway = "%s is now AFK"; ++ public static String afkBroadcastBack = "%s is no longer AFK"; ++ public static String afkTabListPrefix = "[AFK] "; ++ public static String afkTabListSuffix = ""; ++ public static String creditsCommandOutput = "%s has been shown the end credits"; ++ public static String demoCommandOutput = "%s has been shown the demo screen"; ++ public static String pingCommandOutput = "%s's ping is %sms"; ++ public static String ramCommandOutput = "Ram Usage: / ()"; ++ public static String rambarCommandOutput = "Rambar toggled for "; ++ public static String tpsbarCommandOutput = "Tpsbar toggled for "; ++ public static String dontRunWithScissors = "Don't run with scissors!"; ++ public static String uptimeCommandOutput = "Server uptime is "; ++ public static String unverifiedUsername = "default"; ++ public static String sleepSkippingNight = "default"; ++ public static String sleepingPlayersPercent = "default"; ++ private static void messages() { ++ cannotRideMob = getString("settings.messages.cannot-ride-mob", cannotRideMob); ++ afkBroadcastAway = getString("settings.messages.afk-broadcast-away", afkBroadcastAway); ++ afkBroadcastBack = getString("settings.messages.afk-broadcast-back", afkBroadcastBack); ++ afkTabListPrefix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-prefix", afkTabListPrefix))); ++ afkTabListSuffix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-suffix", afkTabListSuffix))); ++ creditsCommandOutput = getString("settings.messages.credits-command-output", creditsCommandOutput); ++ demoCommandOutput = getString("settings.messages.demo-command-output", demoCommandOutput); ++ pingCommandOutput = getString("settings.messages.ping-command-output", pingCommandOutput); ++ ramCommandOutput = getString("settings.messages.ram-command-output", ramCommandOutput); ++ rambarCommandOutput = getString("settings.messages.rambar-command-output", rambarCommandOutput); ++ tpsbarCommandOutput = getString("settings.messages.tpsbar-command-output", tpsbarCommandOutput); ++ dontRunWithScissors = getString("settings.messages.dont-run-with-scissors", dontRunWithScissors); ++ uptimeCommandOutput = getString("settings.messages.uptime-command-output", uptimeCommandOutput); ++ unverifiedUsername = getString("settings.messages.unverified-username", unverifiedUsername); ++ sleepSkippingNight = getString("settings.messages.sleep-skipping-night", sleepSkippingNight); ++ sleepingPlayersPercent = getString("settings.messages.sleeping-players-percent", sleepingPlayersPercent); ++ } ++ ++ public static String deathMsgRunWithScissors = " slipped and fell on their shears"; ++ public static String deathMsgStonecutter = " has sawed themself in half"; ++ private static void deathMessages() { ++ deathMsgRunWithScissors = getString("settings.messages.death-message.run-with-scissors", deathMsgRunWithScissors); ++ deathMsgStonecutter = getString("settings.messages.death-message.stonecutter", deathMsgStonecutter); ++ } ++ ++ public static boolean advancementOnlyBroadcastToAffectedPlayer = false; ++ public static boolean deathMessageOnlyBroadcastToAffectedPlayer = false; ++ private static void broadcastSettings() { ++ if (version < 13) { ++ boolean oldValue = getBoolean("settings.advancement.only-broadcast-to-affected-player", false); ++ set("settings.broadcasts.advancement.only-broadcast-to-affected-player", oldValue); ++ set("settings.advancement.only-broadcast-to-affected-player", null); ++ } ++ advancementOnlyBroadcastToAffectedPlayer = getBoolean("settings.broadcasts.advancement.only-broadcast-to-affected-player", advancementOnlyBroadcastToAffectedPlayer); ++ deathMessageOnlyBroadcastToAffectedPlayer = getBoolean("settings.broadcasts.death.only-broadcast-to-affected-player", deathMessageOnlyBroadcastToAffectedPlayer); ++ } ++ ++ public static String serverModName = "Purpur"; ++ private static void serverModName() { ++ serverModName = getString("settings.server-mod-name", serverModName); ++ } ++ ++ public static double laggingThreshold = 19.0D; ++ private static void tickLoopSettings() { ++ laggingThreshold = getDouble("settings.lagging-threshold", laggingThreshold); ++ } ++ ++ public static boolean useAlternateKeepAlive = false; ++ private static void useAlternateKeepAlive() { ++ useAlternateKeepAlive = getBoolean("settings.use-alternate-keepalive", useAlternateKeepAlive); ++ } ++ ++ public static boolean disableGiveCommandDrops = false; ++ private static void disableGiveCommandDrops() { ++ disableGiveCommandDrops = getBoolean("settings.disable-give-dropping", disableGiveCommandDrops); ++ } ++ ++ public static String commandRamBarTitle = "Ram: / ()"; ++ public static BossBar.Overlay commandRamBarProgressOverlay = BossBar.Overlay.NOTCHED_20; ++ public static BossBar.Color commandRamBarProgressColorGood = BossBar.Color.GREEN; ++ public static BossBar.Color commandRamBarProgressColorMedium = BossBar.Color.YELLOW; ++ public static BossBar.Color commandRamBarProgressColorLow = BossBar.Color.RED; ++ public static String commandRamBarTextColorGood = ""; ++ public static String commandRamBarTextColorMedium = ""; ++ public static String commandRamBarTextColorLow = ""; ++ public static int commandRamBarTickInterval = 20; ++ public static String commandTPSBarTitle = "TPS: MSPT: Ping: ms"; ++ public static BossBar.Overlay commandTPSBarProgressOverlay = BossBar.Overlay.NOTCHED_20; ++ public static TPSBarTask.FillMode commandTPSBarProgressFillMode = TPSBarTask.FillMode.MSPT; ++ public static BossBar.Color commandTPSBarProgressColorGood = BossBar.Color.GREEN; ++ public static BossBar.Color commandTPSBarProgressColorMedium = BossBar.Color.YELLOW; ++ public static BossBar.Color commandTPSBarProgressColorLow = BossBar.Color.RED; ++ public static String commandTPSBarTextColorGood = ""; ++ public static String commandTPSBarTextColorMedium = ""; ++ public static String commandTPSBarTextColorLow = ""; ++ public static int commandTPSBarTickInterval = 20; ++ public static String commandCompassBarTitle = "S \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 W \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 N \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 E \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 S \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 W \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NW \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 N \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 NE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 E \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 SE \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 \u25C8 \u00B7 "; ++ public static BossBar.Overlay commandCompassBarProgressOverlay = BossBar.Overlay.PROGRESS; ++ public static BossBar.Color commandCompassBarProgressColor = BossBar.Color.BLUE; ++ public static float commandCompassBarProgressPercent = 1.0F; ++ public static int commandCompassBarTickInterval = 5; ++ public static boolean commandGamemodeRequiresPermission = false; ++ public static boolean hideHiddenPlayersFromEntitySelector = false; ++ public static String uptimeFormat = ""; ++ public static String uptimeDay = "%02d day, "; ++ public static String uptimeDays = "%02d days, "; ++ public static String uptimeHour = "%02d hour, "; ++ public static String uptimeHours = "%02d hours, "; ++ public static String uptimeMinute = "%02d minute, and "; ++ public static String uptimeMinutes = "%02d minutes, and "; ++ public static String uptimeSecond = "%02d second"; ++ public static String uptimeSeconds = "%02d seconds"; ++ private static void commandSettings() { ++ commandRamBarTitle = getString("settings.command.rambar.title", commandRamBarTitle); ++ commandRamBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.rambar.overlay", commandRamBarProgressOverlay.name())); ++ commandRamBarProgressColorGood = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.good", commandRamBarProgressColorGood.name())); ++ commandRamBarProgressColorMedium = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.medium", commandRamBarProgressColorMedium.name())); ++ commandRamBarProgressColorLow = BossBar.Color.valueOf(getString("settings.command.rambar.progress-color.low", commandRamBarProgressColorLow.name())); ++ commandRamBarTextColorGood = getString("settings.command.rambar.text-color.good", commandRamBarTextColorGood); ++ commandRamBarTextColorMedium = getString("settings.command.rambar.text-color.medium", commandRamBarTextColorMedium); ++ commandRamBarTextColorLow = getString("settings.command.rambar.text-color.low", commandRamBarTextColorLow); ++ commandRamBarTickInterval = getInt("settings.command.rambar.tick-interval", commandRamBarTickInterval); ++ ++ commandTPSBarTitle = getString("settings.command.tpsbar.title", commandTPSBarTitle); ++ commandTPSBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.tpsbar.overlay", commandTPSBarProgressOverlay.name())); ++ commandTPSBarProgressFillMode = TPSBarTask.FillMode.valueOf(getString("settings.command.tpsbar.fill-mode", commandTPSBarProgressFillMode.name())); ++ commandTPSBarProgressColorGood = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.good", commandTPSBarProgressColorGood.name())); ++ commandTPSBarProgressColorMedium = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.medium", commandTPSBarProgressColorMedium.name())); ++ commandTPSBarProgressColorLow = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.low", commandTPSBarProgressColorLow.name())); ++ commandTPSBarTextColorGood = getString("settings.command.tpsbar.text-color.good", commandTPSBarTextColorGood); ++ commandTPSBarTextColorMedium = getString("settings.command.tpsbar.text-color.medium", commandTPSBarTextColorMedium); ++ commandTPSBarTextColorLow = getString("settings.command.tpsbar.text-color.low", commandTPSBarTextColorLow); ++ commandTPSBarTickInterval = getInt("settings.command.tpsbar.tick-interval", commandTPSBarTickInterval); ++ ++ commandCompassBarTitle = getString("settings.command.compass.title", commandCompassBarTitle); ++ commandCompassBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.compass.overlay", commandCompassBarProgressOverlay.name())); ++ commandCompassBarProgressColor = BossBar.Color.valueOf(getString("settings.command.compass.progress-color", commandCompassBarProgressColor.name())); ++ commandCompassBarProgressPercent = (float) getDouble("settings.command.compass.percent", commandCompassBarProgressPercent); ++ commandCompassBarTickInterval = getInt("settings.command.compass.tick-interval", commandCompassBarTickInterval); ++ ++ commandGamemodeRequiresPermission = getBoolean("settings.command.gamemode.requires-specific-permission", commandGamemodeRequiresPermission); ++ hideHiddenPlayersFromEntitySelector = getBoolean("settings.command.hide-hidden-players-from-entity-selector", hideHiddenPlayersFromEntitySelector); ++ uptimeFormat = getString("settings.command.uptime.format", uptimeFormat); ++ uptimeDay = getString("settings.command.uptime.day", uptimeDay); ++ uptimeDays = getString("settings.command.uptime.days", uptimeDays); ++ uptimeHour = getString("settings.command.uptime.hour", uptimeHour); ++ uptimeHours = getString("settings.command.uptime.hours", uptimeHours); ++ uptimeMinute = getString("settings.command.uptime.minute", uptimeMinute); ++ uptimeMinutes = getString("settings.command.uptime.minutes", uptimeMinutes); ++ uptimeSecond = getString("settings.command.uptime.second", uptimeSecond); ++ uptimeSeconds = getString("settings.command.uptime.seconds", uptimeSeconds); ++ } ++ ++ public static int barrelRows = 3; ++ public static boolean enderChestSixRows = false; ++ public static boolean enderChestPermissionRows = false; ++ public static boolean cryingObsidianValidForPortalFrame = false; ++ public static int beeInsideBeeHive = 3; ++ public static boolean anvilCumulativeCost = true; ++ public static int lightningRodRange = 128; ++ public static Set grindstoneIgnoredEnchants = new HashSet<>(); ++ public static boolean grindstoneRemoveAttributes = false; ++ public static boolean grindstoneRemoveDisplay = false; ++ public static int caveVinesMaxGrowthAge = 25; ++ public static int kelpMaxGrowthAge = 25; ++ public static int twistingVinesMaxGrowthAge = 25; ++ public static int weepingVinesMaxGrowthAge = 25; ++ private static void blockSettings() { ++ if (version < 3) { ++ boolean oldValue = getBoolean("settings.barrel.packed-barrels", true); ++ set("settings.blocks.barrel.six-rows", oldValue); ++ set("settings.packed-barrels", null); ++ oldValue = getBoolean("settings.large-ender-chests", true); ++ set("settings.blocks.ender_chest.six-rows", oldValue); ++ set("settings.large-ender-chests", null); ++ } ++ if (version < 20) { ++ boolean oldValue = getBoolean("settings.blocks.barrel.six-rows", false); ++ set("settings.blocks.barrel.rows", oldValue ? 6 : 3); ++ set("settings.blocks.barrel.six-rows", null); ++ } ++ barrelRows = getInt("settings.blocks.barrel.rows", barrelRows); ++ if (barrelRows < 1 || barrelRows > 6) { ++ Bukkit.getLogger().severe("settings.blocks.barrel.rows must be 1-6, resetting to default"); ++ barrelRows = 3; ++ } ++ org.bukkit.event.inventory.InventoryType.BARREL.setDefaultSize(switch (barrelRows) { ++ case 6 -> 54; ++ case 5 -> 45; ++ case 4 -> 36; ++ case 2 -> 18; ++ case 1 -> 9; ++ default -> 27; ++ }); ++ enderChestSixRows = getBoolean("settings.blocks.ender_chest.six-rows", enderChestSixRows); ++ org.bukkit.event.inventory.InventoryType.ENDER_CHEST.setDefaultSize(enderChestSixRows ? 54 : 27); ++ enderChestPermissionRows = getBoolean("settings.blocks.ender_chest.use-permissions-for-rows", enderChestPermissionRows); ++ cryingObsidianValidForPortalFrame = getBoolean("settings.blocks.crying_obsidian.valid-for-portal-frame", cryingObsidianValidForPortalFrame); ++ beeInsideBeeHive = getInt("settings.blocks.beehive.max-bees-inside", beeInsideBeeHive); ++ anvilCumulativeCost = getBoolean("settings.blocks.anvil.cumulative-cost", anvilCumulativeCost); ++ lightningRodRange = getInt("settings.blocks.lightning_rod.range", lightningRodRange); ++ ArrayList defaultCurses = new ArrayList<>(){{ ++ add("minecraft:binding_curse"); ++ add("minecraft:vanishing_curse"); ++ }}; ++ if (version < 24 && !getBoolean("settings.blocks.grindstone.ignore-curses", true)) { ++ defaultCurses.clear(); ++ } ++ getList("settings.blocks.grindstone.ignored-enchants", defaultCurses).forEach(key -> { ++ Enchantment enchantment = BuiltInRegistries.ENCHANTMENT.get(new ResourceLocation(key.toString())); ++ grindstoneIgnoredEnchants.add(enchantment); ++ }); ++ grindstoneRemoveAttributes = getBoolean("settings.blocks.grindstone.remove-attributes", grindstoneRemoveAttributes); ++ grindstoneRemoveDisplay = getBoolean("settings.blocks.grindstone.remove-name-and-lore", grindstoneRemoveDisplay); ++ caveVinesMaxGrowthAge = getInt("settings.blocks.cave_vines.max-growth-age", caveVinesMaxGrowthAge); ++ if (caveVinesMaxGrowthAge > 25) { ++ caveVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.cave_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ kelpMaxGrowthAge = getInt("settings.blocks.kelp.max-growth-age", kelpMaxGrowthAge); ++ if (kelpMaxGrowthAge > 25) { ++ kelpMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.kelp.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ twistingVinesMaxGrowthAge = getInt("settings.blocks.twisting_vines.max-growth-age", twistingVinesMaxGrowthAge); ++ if (twistingVinesMaxGrowthAge > 25) { ++ twistingVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.twisting_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ weepingVinesMaxGrowthAge = getInt("settings.blocks.weeping_vines.max-growth-age", weepingVinesMaxGrowthAge); ++ if (weepingVinesMaxGrowthAge > 25) { ++ weepingVinesMaxGrowthAge = 25; ++ log(Level.WARNING, "blocks.weeping_vines.max-growth-age is set to above maximum allowed value of 25"); ++ log(Level.WARNING, "Using value of 25 to prevent issues"); ++ } ++ } ++ ++ public static boolean allowInfinityMending = false; ++ public static boolean allowCrossbowInfinity = false; ++ public static boolean allowShearsLooting = false; ++ public static boolean allowTransparentBlocksInEnchantmentBox = false; ++ public static boolean allowUnsafeEnchants = false; ++ public static boolean allowInapplicableEnchants = true; ++ public static boolean allowIncompatibleEnchants = true; ++ public static boolean allowHigherEnchantsLevels = true; ++ public static boolean allowUnsafeEnchantCommand = false; ++ public static boolean clampEnchantLevels = true; ++ private static void enchantmentSettings() { ++ if (version < 5) { ++ boolean oldValue = getBoolean("settings.enchantment.allow-infinite-and-mending-together", false); ++ set("settings.enchantment.allow-infinity-and-mending-together", oldValue); ++ set("settings.enchantment.allow-infinite-and-mending-together", null); ++ } ++ if (version < 30) { ++ boolean oldValue = getBoolean("settings.enchantment.allow-unsafe-enchants", false); ++ set("settings.enchantment.anvil.allow-unsafe-enchants", oldValue); ++ set("settings.enchantment.anvil.allow-inapplicable-enchants", true); ++ set("settings.enchantment.anvil.allow-incompatible-enchants", true); ++ set("settings.enchantment.anvil.allow-higher-enchants-levels", true); ++ set("settings.enchantment.allow-unsafe-enchants", null); ++ } ++ allowInfinityMending = getBoolean("settings.enchantment.allow-infinity-and-mending-together", allowInfinityMending); ++ allowCrossbowInfinity = getBoolean("settings.enchantment.allow-infinity-on-crossbow", allowCrossbowInfinity); ++ allowShearsLooting = getBoolean("settings.enchantment.allow-looting-on-shears", allowShearsLooting); ++ allowTransparentBlocksInEnchantmentBox = getBoolean("settings.enchantment.allow-transparent-blocks-in-enchantment-box", allowTransparentBlocksInEnchantmentBox); ++ allowUnsafeEnchants = getBoolean("settings.enchantment.anvil.allow-unsafe-enchants", allowUnsafeEnchants); ++ allowInapplicableEnchants = getBoolean("settings.enchantment.anvil.allow-inapplicable-enchants", allowInapplicableEnchants); ++ allowIncompatibleEnchants = getBoolean("settings.enchantment.anvil.allow-incompatible-enchants", allowIncompatibleEnchants); ++ allowHigherEnchantsLevels = getBoolean("settings.enchantment.anvil.allow-higher-enchants-levels", allowHigherEnchantsLevels); ++ allowUnsafeEnchantCommand = getBoolean("settings.enchantment.allow-unsafe-enchant-command", allowUnsafeEnchants); // allowUnsafeEnchants as default for backwards compatability ++ clampEnchantLevels = getBoolean("settings.enchantment.clamp-levels", clampEnchantLevels); ++ } ++ ++ public static boolean endermanShortHeight = false; ++ private static void entitySettings() { ++ endermanShortHeight = getBoolean("settings.entity.enderman.short-height", endermanShortHeight); ++ if (endermanShortHeight) EntityType.ENDERMAN.setDimensions(EntityDimensions.scalable(0.6F, 1.9F)); ++ } ++ ++ public static boolean allowWaterPlacementInTheEnd = true; ++ private static void allowWaterPlacementInEnd() { ++ allowWaterPlacementInTheEnd = getBoolean("settings.allow-water-placement-in-the-end", allowWaterPlacementInTheEnd); ++ } ++ ++ public static boolean disableMushroomBlockUpdates = false; ++ public static boolean disableNoteBlockUpdates = false; ++ public static boolean disableChorusPlantUpdates = false; ++ private static void blockUpdatesSettings() { ++ disableMushroomBlockUpdates = getBoolean("settings.blocks.disable-mushroom-updates", disableMushroomBlockUpdates); ++ disableNoteBlockUpdates = getBoolean("settings.blocks.disable-note-block-updates", disableNoteBlockUpdates); ++ disableChorusPlantUpdates = getBoolean("settings.blocks.disable-chorus-plant-updates", disableChorusPlantUpdates); ++ } ++ ++ public static boolean loggerSuppressInitLegacyMaterialError = false; ++ public static boolean loggerSuppressIgnoredAdvancementWarnings = false; ++ public static boolean loggerSuppressUnrecognizedRecipeErrors = false; ++ public static boolean loggerSuppressSetBlockFarChunk = false; ++ public static boolean loggerSuppressLibraryLoader = false; ++ private static void loggerSettings() { ++ loggerSuppressInitLegacyMaterialError = getBoolean("settings.logger.suppress-init-legacy-material-errors", loggerSuppressInitLegacyMaterialError); ++ loggerSuppressIgnoredAdvancementWarnings = getBoolean("settings.logger.suppress-ignored-advancement-warnings", loggerSuppressIgnoredAdvancementWarnings); ++ loggerSuppressUnrecognizedRecipeErrors = getBoolean("settings.logger.suppress-unrecognized-recipe-errors", loggerSuppressUnrecognizedRecipeErrors); ++ loggerSuppressSetBlockFarChunk = getBoolean("settings.logger.suppress-setblock-in-far-chunk-errors", loggerSuppressSetBlockFarChunk); ++ loggerSuppressLibraryLoader = getBoolean("settings.logger.suppress-library-loader", loggerSuppressLibraryLoader); ++ org.bukkit.plugin.java.JavaPluginLoader.SuppressLibraryLoaderLogger = loggerSuppressLibraryLoader; ++ } ++ ++ public static boolean tpsCatchup = true; ++ private static void tpsCatchup() { ++ tpsCatchup = getBoolean("settings.tps-catchup", tpsCatchup); ++ } ++ ++ public static boolean useUPnP = false; ++ public static boolean maxJoinsPerSecond = false; ++ public static boolean kickForOutOfOrderChat = true; ++ private static void networkSettings() { ++ useUPnP = getBoolean("settings.network.upnp-port-forwarding", useUPnP); ++ maxJoinsPerSecond = getBoolean("settings.network.max-joins-per-second", maxJoinsPerSecond); ++ kickForOutOfOrderChat = getBoolean("settings.network.kick-for-out-of-order-chat", kickForOutOfOrderChat); ++ } ++ ++ public static java.util.regex.Pattern usernameValidCharactersPattern; ++ private static void usernameValidationSettings() { ++ String defaultPattern = "^[a-zA-Z0-9_.]*$"; ++ String setPattern = getString("settings.username-valid-characters", defaultPattern); ++ usernameValidCharactersPattern = java.util.regex.Pattern.compile(setPattern == null || setPattern.isBlank() ? defaultPattern : setPattern); ++ } ++ ++ private static void foodSettings() { ++ ConfigurationSection properties = config.getConfigurationSection("settings.food-properties"); ++ if (properties == null) { ++ config.addDefault("settings.food-properties", new HashMap<>()); ++ return; ++ } ++ properties.getKeys(false).forEach(foodKey -> { ++ FoodProperties food = Foods.ALL_PROPERTIES.get(foodKey); ++ if (food == null) { ++ PurpurConfig.log(Level.SEVERE, "Invalid food property: " + foodKey); ++ return; ++ } ++ FoodProperties foodDefaults = Foods.DEFAULT_PROPERTIES.get(foodKey); ++ food.setNutrition(properties.getInt(foodKey + ".nutrition", foodDefaults.getNutrition())); ++ food.setSaturationModifier((float) properties.getDouble(foodKey + ".saturation-modifier", foodDefaults.getSaturationModifier())); ++ food.setIsMeat(properties.getBoolean(foodKey + ".is-meat", foodDefaults.isMeat())); ++ food.setCanAlwaysEat(properties.getBoolean(foodKey + ".can-always-eat", foodDefaults.canAlwaysEat())); ++ food.setFastFood(properties.getBoolean(foodKey + ".fast-food", foodDefaults.isFastFood())); ++ ConfigurationSection effects = properties.getConfigurationSection(foodKey + ".effects"); ++ if (effects != null) { ++ Map effectDefaults = new HashMap<>(); ++ foodDefaults.getEffects().forEach(pair -> { ++ effectDefaults.put("chance", pair.getSecond()); ++ MobEffectInstance effect = pair.getFirst(); ++ effectDefaults.put("duration", effect.getDuration()); ++ effectDefaults.put("amplifier", effect.getAmplifier()); ++ effectDefaults.put("ambient", effect.isAmbient()); ++ effectDefaults.put("visible", effect.isVisible()); ++ effectDefaults.put("show-icon", effect.showIcon()); ++ }); ++ effects.getKeys(false).forEach(effectKey -> { ++ MobEffect effect = BuiltInRegistries.MOB_EFFECT.get(new ResourceLocation(effectKey)); ++ if (effect == null) { ++ PurpurConfig.log(Level.SEVERE, "Invalid food property effect for " + foodKey + ": " + effectKey); ++ return; ++ } ++ food.getEffects().removeIf(pair -> pair.getFirst().getEffect() == effect); ++ float chance = (float) effects.getDouble(effectKey + ".chance", ((Float) effectDefaults.get("chance")).doubleValue()); ++ int duration = effects.getInt(effectKey + ".duration", (int) effectDefaults.get("duration")); ++ if (chance <= 0.0F || duration < 0) { ++ return; ++ } ++ int amplifier = effects.getInt(effectKey + ".amplifier", (int) effectDefaults.get("amplifier")); ++ boolean ambient = effects.getBoolean(effectKey + ".ambient", (boolean) effectDefaults.get("ambient")); ++ boolean visible = effects.getBoolean(effectKey + ".visible", (boolean) effectDefaults.get("visible")); ++ boolean showIcon = effects.getBoolean(effectKey + ".show-icon", (boolean) effectDefaults.get("show-icon")); ++ food.getEffects().add(Pair.of(new MobEffectInstance(effect, duration, amplifier, ambient, visible, showIcon), chance)); ++ }); ++ } ++ }); ++ } ++ ++ public static boolean fixNetworkSerializedItemsInCreative = false; ++ private static void fixNetworkSerializedCreativeItems() { ++ fixNetworkSerializedItemsInCreative = getBoolean("settings.fix-network-serialized-items-in-creative", fixNetworkSerializedItemsInCreative); ++ } ++ ++ public static boolean fixProjectileLootingTransfer = false; ++ private static void fixProjectileLootingTransfer() { ++ fixProjectileLootingTransfer = getBoolean("settings.fix-projectile-looting-transfer", fixProjectileLootingTransfer); ++ } ++ ++ public static boolean clampAttributes = true; ++ private static void clampAttributes() { ++ clampAttributes = getBoolean("settings.clamp-attributes", clampAttributes); ++ } ++ ++ public static boolean limitArmor = true; ++ private static void limitArmor() { ++ limitArmor = getBoolean("settings.limit-armor", limitArmor); ++ } ++ ++ private static void blastResistanceSettings() { ++ getMap("settings.blast-resistance-overrides", Collections.emptyMap()).forEach((blockId, value) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { ++ log(Level.SEVERE, "Invalid block for `settings.blast-resistance-overrides`: " + blockId); ++ return; ++ } ++ if (!(value instanceof Number blastResistance)) { ++ log(Level.SEVERE, "Invalid blast resistance for `settings.blast-resistance-overrides." + blockId + "`: " + value); ++ return; ++ } ++ block.explosionResistance = blastResistance.floatValue(); ++ }); ++ } ++ private static void blockFallMultiplierSettings() { ++ getMap("settings.block-fall-multipliers", Map.ofEntries( ++ Map.entry("minecraft:hay_block", Map.of("damage", 0.2F)), ++ Map.entry("minecraft:white_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:light_gray_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:gray_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:black_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:brown_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:pink_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:red_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:orange_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:yellow_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:green_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:lime_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:cyan_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:light_blue_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:blue_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:purple_bed", Map.of("distance", 0.5F)), ++ Map.entry("minecraft:magenta_bed", Map.of("distance", 0.5F)) ++ )).forEach((blockId, value) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { ++ log(Level.SEVERE, "Invalid block for `settings.block-fall-multipliers`: " + blockId); ++ return; ++ } ++ if (!(value instanceof Map map)) { ++ log(Level.SEVERE, "Invalid fall multiplier for `settings.block-fall-multipliers." + blockId + "`: " + value ++ + ", expected a map with keys `damage` and `distance` to floats."); ++ return; ++ } ++ Object rawFallDamageMultiplier = map.get("damage"); ++ if (rawFallDamageMultiplier == null) rawFallDamageMultiplier = 1F; ++ if (!(rawFallDamageMultiplier instanceof Number fallDamageMultiplier)) { ++ log(Level.SEVERE, "Invalid multiplier for `settings.block-fall-multipliers." + blockId + ".damage`: " + map.get("damage")); ++ return; ++ } ++ Object rawFallDistanceMultiplier = map.get("distance"); ++ if (rawFallDistanceMultiplier == null) rawFallDistanceMultiplier = 1F; ++ if (!(rawFallDistanceMultiplier instanceof Number fallDistanceMultiplier)) { ++ log(Level.SEVERE, "Invalid multiplier for `settings.block-fall-multipliers." + blockId + ".distance`: " + map.get("distance")); ++ return; ++ } ++ block.fallDamageMultiplier = fallDamageMultiplier.floatValue(); ++ block.fallDistanceMultiplier = fallDistanceMultiplier.floatValue(); ++ }); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0db8a1a51c857a3930d0b20028964fb355d8e5b4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +@@ -0,0 +1,3201 @@ ++package org.purpurmc.purpur; ++ ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.item.DyeColor; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.level.Explosion; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.properties.Tilt; ++import org.purpurmc.purpur.entity.GlowSquidColor; ++import org.purpurmc.purpur.tool.Strippable; ++import org.purpurmc.purpur.tool.Tillable; ++import org.purpurmc.purpur.tool.Waxable; ++import org.purpurmc.purpur.tool.Weatherable; ++import org.apache.commons.lang.BooleanUtils; ++import org.bukkit.ChatColor; ++import org.bukkit.World; ++import org.bukkit.configuration.ConfigurationSection; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.function.Predicate; ++import java.util.logging.Level; ++import static org.purpurmc.purpur.PurpurConfig.log; ++ ++@SuppressWarnings("unused") ++public class PurpurWorldConfig { ++ ++ private final String worldName; ++ private final World.Environment environment; ++ ++ public PurpurWorldConfig(String worldName, World.Environment environment) { ++ this.worldName = worldName; ++ this.environment = environment; ++ init(); ++ } ++ ++ public void init() { ++ log("-------- World Settings For [" + worldName + "] --------"); ++ PurpurConfig.readConfig(PurpurWorldConfig.class, this); ++ } ++ ++ private void set(String path, Object val) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, val); ++ PurpurConfig.config.set("world-settings.default." + path, val); ++ if (PurpurConfig.config.get("world-settings." + worldName + "." + path) != null) { ++ PurpurConfig.config.addDefault("world-settings." + worldName + "." + path, val); ++ PurpurConfig.config.set("world-settings." + worldName + "." + path, val); ++ } ++ } ++ ++ private ConfigurationSection getConfigurationSection(String path) { ++ ConfigurationSection section = PurpurConfig.config.getConfigurationSection("world-settings." + worldName + "." + path); ++ return section != null ? section : PurpurConfig.config.getConfigurationSection("world-settings.default." + path); ++ } ++ ++ private String getString(String path, String def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getString("world-settings." + worldName + "." + path, PurpurConfig.config.getString("world-settings.default." + path)); ++ } ++ ++ private boolean getBoolean(String path, boolean def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getBoolean("world-settings." + worldName + "." + path, PurpurConfig.config.getBoolean("world-settings.default." + path)); ++ } ++ ++ private boolean getBoolean(String path, Predicate predicate) { ++ String val = getString(path, "default").toLowerCase(); ++ Boolean bool = BooleanUtils.toBooleanObject(val, "true", "false", "default"); ++ return predicate.test(bool); ++ } ++ ++ private double getDouble(String path, double def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getDouble("world-settings." + worldName + "." + path, PurpurConfig.config.getDouble("world-settings.default." + path)); ++ } ++ ++ private int getInt(String path, int def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getInt("world-settings." + worldName + "." + path, PurpurConfig.config.getInt("world-settings.default." + path)); ++ } ++ ++ private List getList(String path, T def) { ++ PurpurConfig.config.addDefault("world-settings.default." + path, def); ++ return PurpurConfig.config.getList("world-settings." + worldName + "." + path, PurpurConfig.config.getList("world-settings.default." + path)); ++ } ++ ++ private Map getMap(String path, Map def) { ++ final Map fallback = PurpurConfig.getMap("world-settings.default." + path, def); ++ final Map value = PurpurConfig.getMap("world-settings." + worldName + "." + path, null); ++ return value.isEmpty() ? fallback : value; ++ } ++ ++ public float armorstandStepHeight = 0.0F; ++ public boolean armorstandSetNameVisible = true; ++ public boolean armorstandFixNametags = false; ++ public boolean armorstandMovement = true; ++ public boolean armorstandWaterMovement = true; ++ public boolean armorstandWaterFence = true; ++ public boolean armorstandPlaceWithArms = false; ++ private void armorstandSettings() { ++ armorstandStepHeight = (float) getDouble("gameplay-mechanics.armorstand.step-height", armorstandStepHeight); ++ armorstandSetNameVisible = getBoolean("gameplay-mechanics.armorstand.set-name-visible-when-placing-with-custom-name", armorstandSetNameVisible); ++ armorstandFixNametags = getBoolean("gameplay-mechanics.armorstand.fix-nametags", armorstandFixNametags); ++ armorstandMovement = getBoolean("gameplay-mechanics.armorstand.can-movement-tick", armorstandMovement); ++ armorstandWaterMovement = getBoolean("gameplay-mechanics.armorstand.can-move-in-water", armorstandWaterMovement); ++ armorstandWaterFence = getBoolean("gameplay-mechanics.armorstand.can-move-in-water-over-fence", armorstandWaterFence); ++ armorstandPlaceWithArms = getBoolean("gameplay-mechanics.armorstand.place-with-arms-visible", armorstandPlaceWithArms); ++ } ++ ++ public boolean arrowMovementResetsDespawnCounter = true; ++ private void arrowSettings() { ++ arrowMovementResetsDespawnCounter = getBoolean("gameplay-mechanics.arrow.movement-resets-despawn-counter", arrowMovementResetsDespawnCounter); ++ } ++ ++ public boolean useBetterMending = false; ++ public boolean alwaysTameInCreative = false; ++ public boolean boatEjectPlayersOnLand = false; ++ public boolean boatsDoFallDamage = false; ++ public boolean disableDropsOnCrammingDeath = false; ++ public boolean entitiesCanUsePortals = true; ++ public boolean entitiesPickUpLootBypassMobGriefing = false; ++ public boolean fireballsBypassMobGriefing = false; ++ public boolean imposeTeleportRestrictionsOnGateways = false; ++ public boolean milkCuresBadOmen = true; ++ public boolean milkClearsBeneficialEffects = true; ++ public boolean noteBlockIgnoreAbove = false; ++ public boolean persistentDroppableEntityDisplayNames = true; ++ public boolean persistentTileEntityDisplayNames = false; ++ public boolean projectilesBypassMobGriefing = false; ++ public boolean tickFluids = true; ++ public double mobsBlindnessMultiplier = 1; ++ public double tridentLoyaltyVoidReturnHeight = 0.0D; ++ public double voidDamageHeight = -64.0D; ++ public double voidDamageDealt = 4.0D; ++ public int raidCooldownSeconds = 0; ++ public int animalBreedingCooldownSeconds = 0; ++ public boolean mobsIgnoreRails = false; ++ public boolean rainStopsAfterSleep = true; ++ public boolean thunderStopsAfterSleep = true; ++ public int mobLastHurtByPlayerTime = 100; ++ private void miscGameplayMechanicsSettings() { ++ useBetterMending = getBoolean("gameplay-mechanics.use-better-mending", useBetterMending); ++ alwaysTameInCreative = getBoolean("gameplay-mechanics.always-tame-in-creative", alwaysTameInCreative); ++ boatEjectPlayersOnLand = getBoolean("gameplay-mechanics.boat.eject-players-on-land", boatEjectPlayersOnLand); ++ boatsDoFallDamage = getBoolean("gameplay-mechanics.boat.do-fall-damage", boatsDoFallDamage); ++ disableDropsOnCrammingDeath = getBoolean("gameplay-mechanics.disable-drops-on-cramming-death", disableDropsOnCrammingDeath); ++ entitiesCanUsePortals = getBoolean("gameplay-mechanics.entities-can-use-portals", entitiesCanUsePortals); ++ entitiesPickUpLootBypassMobGriefing = getBoolean("gameplay-mechanics.entities-pick-up-loot-bypass-mob-griefing", entitiesPickUpLootBypassMobGriefing); ++ fireballsBypassMobGriefing = getBoolean("gameplay-mechanics.fireballs-bypass-mob-griefing", fireballsBypassMobGriefing); ++ imposeTeleportRestrictionsOnGateways = getBoolean("gameplay-mechanics.impose-teleport-restrictions-on-gateways", imposeTeleportRestrictionsOnGateways); ++ milkCuresBadOmen = getBoolean("gameplay-mechanics.milk-cures-bad-omen", milkCuresBadOmen); ++ milkClearsBeneficialEffects = getBoolean("gameplay-mechanics.milk-clears-beneficial-effects", milkClearsBeneficialEffects); ++ noteBlockIgnoreAbove = getBoolean("gameplay-mechanics.note-block-ignore-above", noteBlockIgnoreAbove); ++ persistentTileEntityDisplayNames = getBoolean("gameplay-mechanics.persistent-tileentity-display-names-and-lore", persistentTileEntityDisplayNames); ++ persistentDroppableEntityDisplayNames = getBoolean("gameplay-mechanics.persistent-droppable-entity-display-names", persistentDroppableEntityDisplayNames); ++ projectilesBypassMobGriefing = getBoolean("gameplay-mechanics.projectiles-bypass-mob-griefing", projectilesBypassMobGriefing); ++ tickFluids = getBoolean("gameplay-mechanics.tick-fluids", tickFluids); ++ mobsBlindnessMultiplier = getDouble("gameplay-mechanics.entity-blindness-multiplier", mobsBlindnessMultiplier); ++ tridentLoyaltyVoidReturnHeight = getDouble("gameplay-mechanics.trident-loyalty-void-return-height", tridentLoyaltyVoidReturnHeight); ++ voidDamageHeight = getDouble("gameplay-mechanics.void-damage-height", voidDamageHeight); ++ voidDamageDealt = getDouble("gameplay-mechanics.void-damage-dealt", voidDamageDealt); ++ raidCooldownSeconds = getInt("gameplay-mechanics.raid-cooldown-seconds", raidCooldownSeconds); ++ animalBreedingCooldownSeconds = getInt("gameplay-mechanics.animal-breeding-cooldown-seconds", animalBreedingCooldownSeconds); ++ mobsIgnoreRails = getBoolean("gameplay-mechanics.mobs-ignore-rails", mobsIgnoreRails); ++ rainStopsAfterSleep = getBoolean("gameplay-mechanics.rain-stops-after-sleep", rainStopsAfterSleep); ++ thunderStopsAfterSleep = getBoolean("gameplay-mechanics.thunder-stops-after-sleep", thunderStopsAfterSleep); ++ mobLastHurtByPlayerTime = getInt("gameplay-mechanics.mob-last-hurt-by-player-time", mobLastHurtByPlayerTime); ++ } ++ ++ public int daytimeTicks = 12000; ++ public int nighttimeTicks = 12000; ++ private void daytimeCycleSettings() { ++ daytimeTicks = getInt("gameplay-mechanics.daylight-cycle-ticks.daytime", daytimeTicks); ++ nighttimeTicks = getInt("gameplay-mechanics.daylight-cycle-ticks.nighttime", nighttimeTicks); ++ } ++ ++ public int drowningAirTicks = 300; ++ public int drowningDamageInterval = 20; ++ public double damageFromDrowning = 2.0F; ++ private void drowningSettings() { ++ drowningAirTicks = getInt("gameplay-mechanics.drowning.air-ticks", drowningAirTicks); ++ drowningDamageInterval = getInt("gameplay-mechanics.drowning.ticks-per-damage", drowningDamageInterval); ++ damageFromDrowning = getDouble("gameplay-mechanics.drowning.damage-from-drowning", damageFromDrowning); ++ } ++ ++ public int elytraDamagePerSecond = 1; ++ public double elytraDamageMultiplyBySpeed = 0; ++ public boolean elytraIgnoreUnbreaking = false; ++ public int elytraDamagePerFireworkBoost = 0; ++ public int elytraDamagePerTridentBoost = 0; ++ public boolean elytraKineticDamage = true; ++ private void elytraSettings() { ++ elytraDamagePerSecond = getInt("gameplay-mechanics.elytra.damage-per-second", elytraDamagePerSecond); ++ elytraDamageMultiplyBySpeed = getDouble("gameplay-mechanics.elytra.damage-multiplied-by-speed", elytraDamageMultiplyBySpeed); ++ elytraIgnoreUnbreaking = getBoolean("gameplay-mechanics.elytra.ignore-unbreaking", elytraIgnoreUnbreaking); ++ elytraDamagePerFireworkBoost = getInt("gameplay-mechanics.elytra.damage-per-boost.firework", elytraDamagePerFireworkBoost); ++ elytraDamagePerTridentBoost = getInt("gameplay-mechanics.elytra.damage-per-boost.trident", elytraDamagePerTridentBoost); ++ elytraKineticDamage = getBoolean("gameplay-mechanics.elytra.kinetic-damage", elytraKineticDamage); ++ } ++ ++ public int entityLifeSpan = 0; ++ public float entityLeftHandedChance = 0.05f; ++ public boolean entitySharedRandom = true; ++ private void entitySettings() { ++ entityLifeSpan = getInt("gameplay-mechanics.entity-lifespan", entityLifeSpan); ++ entityLeftHandedChance = (float) getDouble("gameplay-mechanics.entity-left-handed-chance", entityLeftHandedChance); ++ entitySharedRandom = getBoolean("settings.entity.shared-random", entitySharedRandom); ++ } ++ ++ public boolean explosionClampRadius = true; ++ private void explosionSettings() { ++ explosionClampRadius = getBoolean("gameplay-mechanics.clamp-explosion-radius", explosionClampRadius); ++ } ++ ++ public boolean infinityWorksWithoutArrows = false; ++ public boolean infinityWorksWithNormalArrows = true; ++ public boolean infinityWorksWithSpectralArrows = false; ++ public boolean infinityWorksWithTippedArrows = false; ++ private void infinityArrowsSettings() { ++ infinityWorksWithoutArrows = getBoolean("gameplay-mechanics.infinity-bow.works-without-arrows", infinityWorksWithoutArrows); ++ infinityWorksWithNormalArrows = getBoolean("gameplay-mechanics.infinity-bow.normal-arrows", infinityWorksWithNormalArrows); ++ infinityWorksWithSpectralArrows = getBoolean("gameplay-mechanics.infinity-bow.spectral-arrows", infinityWorksWithSpectralArrows); ++ infinityWorksWithTippedArrows = getBoolean("gameplay-mechanics.infinity-bow.tipped-arrows", infinityWorksWithTippedArrows); ++ } ++ ++ public List itemImmuneToCactus = new ArrayList<>(); ++ public List itemImmuneToExplosion = new ArrayList<>(); ++ public List itemImmuneToFire = new ArrayList<>(); ++ public List itemImmuneToLightning = new ArrayList<>(); ++ public boolean dontRunWithScissors = false; ++ public boolean ignoreScissorsInWater = false; ++ public boolean ignoreScissorsInLava = false; ++ public double scissorsRunningDamage = 1D; ++ public float enderPearlDamage = 5.0F; ++ public int enderPearlCooldown = 20; ++ public int enderPearlCooldownCreative = 20; ++ public float enderPearlEndermiteChance = 0.05F; ++ public int glowBerriesEatGlowDuration = 0; ++ public boolean shulkerBoxItemDropContentsWhenDestroyed = true; ++ public boolean compassItemShowsBossBar = false; ++ public boolean snowballExtinguishesFire = false; ++ public boolean snowballExtinguishesCandles = false; ++ public boolean snowballExtinguishesCampfires = false; ++ private void itemSettings() { ++ itemImmuneToCactus.clear(); ++ getList("gameplay-mechanics.item.immune.cactus", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToCactus.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToCactus.add(item); ++ }); ++ itemImmuneToExplosion.clear(); ++ getList("gameplay-mechanics.item.immune.explosion", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToExplosion.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToExplosion.add(item); ++ }); ++ itemImmuneToFire.clear(); ++ getList("gameplay-mechanics.item.immune.fire", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToFire.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToFire.add(item); ++ }); ++ itemImmuneToLightning.clear(); ++ getList("gameplay-mechanics.item.immune.lightning", new ArrayList<>()).forEach(key -> { ++ if (key.toString().equals("*")) { ++ BuiltInRegistries.ITEM.stream().filter(item -> item != Items.AIR).forEach((item) -> itemImmuneToLightning.add(item)); ++ return; ++ } ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) itemImmuneToLightning.add(item); ++ }); ++ dontRunWithScissors = getBoolean("gameplay-mechanics.item.shears.damage-if-sprinting", dontRunWithScissors); ++ ignoreScissorsInWater = getBoolean("gameplay-mechanics.item.shears.ignore-in-water", ignoreScissorsInWater); ++ ignoreScissorsInLava = getBoolean("gameplay-mechanics.item.shears.ignore-in-lava", ignoreScissorsInLava); ++ scissorsRunningDamage = getDouble("gameplay-mechanics.item.shears.sprinting-damage", scissorsRunningDamage); ++ enderPearlDamage = (float) getDouble("gameplay-mechanics.item.ender-pearl.damage", enderPearlDamage); ++ enderPearlCooldown = getInt("gameplay-mechanics.item.ender-pearl.cooldown", enderPearlCooldown); ++ enderPearlCooldownCreative = getInt("gameplay-mechanics.item.ender-pearl.creative-cooldown", enderPearlCooldownCreative); ++ enderPearlEndermiteChance = (float) getDouble("gameplay-mechanics.item.ender-pearl.endermite-spawn-chance", enderPearlEndermiteChance); ++ glowBerriesEatGlowDuration = getInt("gameplay-mechanics.item.glow_berries.eat-glow-duration", glowBerriesEatGlowDuration); ++ shulkerBoxItemDropContentsWhenDestroyed = getBoolean("gameplay-mechanics.item.shulker_box.drop-contents-when-destroyed", shulkerBoxItemDropContentsWhenDestroyed); ++ compassItemShowsBossBar = getBoolean("gameplay-mechanics.item.compass.holding-shows-bossbar", compassItemShowsBossBar); ++ snowballExtinguishesFire = getBoolean("gameplay-mechanics.item.snowball.extinguish.fire", snowballExtinguishesFire); ++ snowballExtinguishesCandles = getBoolean("gameplay-mechanics.item.snowball.extinguish.candles", snowballExtinguishesCandles); ++ snowballExtinguishesCampfires = getBoolean("gameplay-mechanics.item.snowball.extinguish.campfires", snowballExtinguishesCampfires); ++ } ++ ++ public double minecartMaxSpeed = 0.4D; ++ public boolean minecartPlaceAnywhere = false; ++ public boolean minecartControllable = false; ++ public float minecartControllableStepHeight = 1.0F; ++ public double minecartControllableHopBoost = 0.5D; ++ public boolean minecartControllableFallDamage = true; ++ public double minecartControllableBaseSpeed = 0.1D; ++ public Map minecartControllableBlockSpeeds = new HashMap<>(); ++ public double poweredRailBoostModifier = 0.06; ++ private void minecartSettings() { ++ if (PurpurConfig.version < 12) { ++ boolean oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.place-anywhere", minecartPlaceAnywhere); ++ set("gameplay-mechanics.controllable-minecarts.place-anywhere", null); ++ set("gameplay-mechanics.minecart.place-anywhere", oldBool); ++ oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.enabled", minecartControllable); ++ set("gameplay-mechanics.controllable-minecarts.enabled", null); ++ set("gameplay-mechanics.minecart.controllable.enabled", oldBool); ++ double oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.step-height", minecartControllableStepHeight); ++ set("gameplay-mechanics.controllable-minecarts.step-height", null); ++ set("gameplay-mechanics.minecart.controllable.step-height", oldDouble); ++ oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.hop-boost", minecartControllableHopBoost); ++ set("gameplay-mechanics.controllable-minecarts.hop-boost", null); ++ set("gameplay-mechanics.minecart.controllable.hop-boost", oldDouble); ++ oldBool = getBoolean("gameplay-mechanics.controllable-minecarts.fall-damage", minecartControllableFallDamage); ++ set("gameplay-mechanics.controllable-minecarts.fall-damage", null); ++ set("gameplay-mechanics.minecart.controllable.fall-damage", oldBool); ++ oldDouble = getDouble("gameplay-mechanics.controllable-minecarts.base-speed", minecartControllableBaseSpeed); ++ set("gameplay-mechanics.controllable-minecarts.base-speed", null); ++ set("gameplay-mechanics.minecart.controllable.base-speed", oldDouble); ++ ConfigurationSection section = getConfigurationSection("gameplay-mechanics.controllable-minecarts.block-speed"); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ if ("grass-block".equals(key)) key = "grass_block"; // oopsie ++ oldDouble = section.getDouble(key, minecartControllableBaseSpeed); ++ set("gameplay-mechanics.controllable-minecarts.block-speed." + key, null); ++ set("gameplay-mechanics.minecart.controllable.block-speed." + key, oldDouble); ++ } ++ set("gameplay-mechanics.controllable-minecarts.block-speed", null); ++ } ++ set("gameplay-mechanics.controllable-minecarts", null); ++ } ++ ++ minecartMaxSpeed = getDouble("gameplay-mechanics.minecart.max-speed", minecartMaxSpeed); ++ minecartPlaceAnywhere = getBoolean("gameplay-mechanics.minecart.place-anywhere", minecartPlaceAnywhere); ++ minecartControllable = getBoolean("gameplay-mechanics.minecart.controllable.enabled", minecartControllable); ++ minecartControllableStepHeight = (float) getDouble("gameplay-mechanics.minecart.controllable.step-height", minecartControllableStepHeight); ++ minecartControllableHopBoost = getDouble("gameplay-mechanics.minecart.controllable.hop-boost", minecartControllableHopBoost); ++ minecartControllableFallDamage = getBoolean("gameplay-mechanics.minecart.controllable.fall-damage", minecartControllableFallDamage); ++ minecartControllableBaseSpeed = getDouble("gameplay-mechanics.minecart.controllable.base-speed", minecartControllableBaseSpeed); ++ ConfigurationSection section = getConfigurationSection("gameplay-mechanics.minecart.controllable.block-speed"); ++ if (section != null) { ++ for (String key : section.getKeys(false)) { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key)); ++ if (block != Blocks.AIR) { ++ minecartControllableBlockSpeeds.put(block, section.getDouble(key, minecartControllableBaseSpeed)); ++ } ++ } ++ } else { ++ set("gameplay-mechanics.minecart.controllable.block-speed.grass_block", 0.3D); ++ set("gameplay-mechanics.minecart.controllable.block-speed.stone", 0.5D); ++ } ++ poweredRailBoostModifier = getDouble("gameplay-mechanics.minecart.powered-rail.boost-modifier", poweredRailBoostModifier); ++ } ++ ++ public float entityHealthRegenAmount = 1.0F; ++ public float entityMinimalHealthPoison = 1.0F; ++ public float entityPoisonDegenerationAmount = 1.0F; ++ public float entityWitherDegenerationAmount = 1.0F; ++ public float humanHungerExhaustionAmount = 0.005F; ++ public float humanSaturationRegenAmount = 1.0F; ++ private void mobEffectSettings() { ++ entityHealthRegenAmount = (float) getDouble("gameplay-mechanics.mob-effects.health-regen-amount", entityHealthRegenAmount); ++ entityMinimalHealthPoison = (float) getDouble("gameplay-mechanics.mob-effects.minimal-health-poison-amount", entityMinimalHealthPoison); ++ entityPoisonDegenerationAmount = (float) getDouble("gameplay-mechanics.mob-effects.poison-degeneration-amount", entityPoisonDegenerationAmount); ++ entityWitherDegenerationAmount = (float) getDouble("gameplay-mechanics.mob-effects.wither-degeneration-amount", entityWitherDegenerationAmount); ++ humanHungerExhaustionAmount = (float) getDouble("gameplay-mechanics.mob-effects.hunger-exhaustion-amount", humanHungerExhaustionAmount); ++ humanSaturationRegenAmount = (float) getDouble("gameplay-mechanics.mob-effects.saturation-regen-amount", humanSaturationRegenAmount); ++ } ++ ++ public boolean catSpawning; ++ public boolean patrolSpawning; ++ public boolean phantomSpawning; ++ public boolean villagerTraderSpawning; ++ public boolean villageSiegeSpawning; ++ public boolean mobSpawningIgnoreCreativePlayers = false; ++ private void mobSpawnerSettings() { ++ // values of "default" or null will default to true only if the world environment is normal (aka overworld) ++ Predicate predicate = (bool) -> (bool != null && bool) || (bool == null && environment == World.Environment.NORMAL); ++ catSpawning = getBoolean("gameplay-mechanics.mob-spawning.village-cats", predicate); ++ patrolSpawning = getBoolean("gameplay-mechanics.mob-spawning.raid-patrols", predicate); ++ phantomSpawning = getBoolean("gameplay-mechanics.mob-spawning.phantoms", predicate); ++ villagerTraderSpawning = getBoolean("gameplay-mechanics.mob-spawning.wandering-traders", predicate); ++ villageSiegeSpawning = getBoolean("gameplay-mechanics.mob-spawning.village-sieges", predicate); ++ mobSpawningIgnoreCreativePlayers = getBoolean("gameplay-mechanics.mob-spawning.ignore-creative-players", mobSpawningIgnoreCreativePlayers); ++ } ++ ++ public boolean disableObserverClocks = false; ++ private void observerSettings() { ++ disableObserverClocks = getBoolean("blocks.observer.disable-clock", disableObserverClocks); ++ } ++ ++ public int playerNetheriteFireResistanceDuration = 0; ++ public int playerNetheriteFireResistanceAmplifier = 0; ++ public boolean playerNetheriteFireResistanceAmbient = false; ++ public boolean playerNetheriteFireResistanceShowParticles = false; ++ public boolean playerNetheriteFireResistanceShowIcon = true; ++ private void playerNetheriteFireResistance() { ++ playerNetheriteFireResistanceDuration = getInt("gameplay-mechanics.player.netherite-fire-resistance.duration", playerNetheriteFireResistanceDuration); ++ playerNetheriteFireResistanceAmplifier = getInt("gameplay-mechanics.player.netherite-fire-resistance.amplifier", playerNetheriteFireResistanceAmplifier); ++ playerNetheriteFireResistanceAmbient = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.ambient", playerNetheriteFireResistanceAmbient); ++ playerNetheriteFireResistanceShowParticles = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.show-particles", playerNetheriteFireResistanceShowParticles); ++ playerNetheriteFireResistanceShowIcon = getBoolean("gameplay-mechanics.player.netherite-fire-resistance.show-icon", playerNetheriteFireResistanceShowIcon); ++ } ++ ++ public boolean idleTimeoutKick = true; ++ public boolean idleTimeoutTickNearbyEntities = true; ++ public boolean idleTimeoutCountAsSleeping = false; ++ public boolean idleTimeoutUpdateTabList = false; ++ public boolean idleTimeoutTargetPlayer = true; ++ public int playerSpawnInvulnerableTicks = 60; ++ public boolean playerInvulnerableWhileAcceptingResourcePack = false; ++ public String playerDeathExpDropEquation = "expLevel * 7"; ++ public int playerDeathExpDropMax = 100; ++ public boolean teleportIfOutsideBorder = false; ++ public boolean teleportOnNetherCeilingDamage = false; ++ public boolean totemOfUndyingWorksInInventory = false; ++ public boolean playerFixStuckPortal = false; ++ public boolean creativeOnePunch = false; ++ public boolean playerSleepNearMonsters = false; ++ public boolean playersSkipNight = true; ++ public double playerCriticalDamageMultiplier = 1.5D; ++ public int playerBurpDelay = 10; ++ public boolean playerBurpWhenFull = false; ++ public int playerPortalWaitTime = 80; ++ public int playerCreativePortalWaitTime = 1; ++ public boolean playerRidableInWater = false; ++ public boolean playerRemoveBindingWithWeakness = false; ++ public int shiftRightClickRepairsMendingPoints = 0; ++ public int playerExpPickupDelay = 2; ++ public boolean playerVoidTrading = false; ++ private void playerSettings() { ++ if (PurpurConfig.version < 19) { ++ boolean oldVal = getBoolean("gameplay-mechanics.player.idle-timeout.mods-target", idleTimeoutTargetPlayer); ++ set("gameplay-mechanics.player.idle-timeout.mods-target", null); ++ set("gameplay-mechanics.player.idle-timeout.mobs-target", oldVal); ++ } ++ idleTimeoutKick = System.getenv("PURPUR_FORCE_IDLE_KICK") == null ? getBoolean("gameplay-mechanics.player.idle-timeout.kick-if-idle", idleTimeoutKick) : Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")); ++ idleTimeoutTickNearbyEntities = getBoolean("gameplay-mechanics.player.idle-timeout.tick-nearby-entities", idleTimeoutTickNearbyEntities); ++ idleTimeoutCountAsSleeping = getBoolean("gameplay-mechanics.player.idle-timeout.count-as-sleeping", idleTimeoutCountAsSleeping); ++ idleTimeoutUpdateTabList = getBoolean("gameplay-mechanics.player.idle-timeout.update-tab-list", idleTimeoutUpdateTabList); ++ idleTimeoutTargetPlayer = getBoolean("gameplay-mechanics.player.idle-timeout.mobs-target", idleTimeoutTargetPlayer); ++ playerSpawnInvulnerableTicks = getInt("gameplay-mechanics.player.spawn-invulnerable-ticks", playerSpawnInvulnerableTicks); ++ playerInvulnerableWhileAcceptingResourcePack = getBoolean("gameplay-mechanics.player.invulnerable-while-accepting-resource-pack", playerInvulnerableWhileAcceptingResourcePack); ++ playerDeathExpDropEquation = getString("gameplay-mechanics.player.exp-dropped-on-death.equation", playerDeathExpDropEquation); ++ playerDeathExpDropMax = getInt("gameplay-mechanics.player.exp-dropped-on-death.maximum", playerDeathExpDropMax); ++ teleportIfOutsideBorder = getBoolean("gameplay-mechanics.player.teleport-if-outside-border", teleportIfOutsideBorder); ++ teleportOnNetherCeilingDamage = getBoolean("gameplay-mechanics.player.teleport-on-nether-ceiling-damage", teleportOnNetherCeilingDamage); ++ totemOfUndyingWorksInInventory = getBoolean("gameplay-mechanics.player.totem-of-undying-works-in-inventory", totemOfUndyingWorksInInventory); ++ playerFixStuckPortal = getBoolean("gameplay-mechanics.player.fix-stuck-in-portal", playerFixStuckPortal); ++ creativeOnePunch = getBoolean("gameplay-mechanics.player.one-punch-in-creative", creativeOnePunch); ++ playerSleepNearMonsters = getBoolean("gameplay-mechanics.player.sleep-ignore-nearby-mobs", playerSleepNearMonsters); ++ playersSkipNight = getBoolean("gameplay-mechanics.player.can-skip-night", playersSkipNight); ++ playerCriticalDamageMultiplier = getDouble("gameplay-mechanics.player.critical-damage-multiplier", playerCriticalDamageMultiplier); ++ playerBurpDelay = getInt("gameplay-mechanics.player.burp-delay", playerBurpDelay); ++ playerBurpWhenFull = getBoolean("gameplay-mechanics.player.burp-when-full", playerBurpWhenFull); ++ playerPortalWaitTime = getInt("gameplay-mechanics.player.portal-wait-time", playerPortalWaitTime); ++ playerCreativePortalWaitTime = getInt("gameplay-mechanics.player.creative-portal-wait-time", playerCreativePortalWaitTime); ++ playerRidableInWater = getBoolean("gameplay-mechanics.player.ridable-in-water", playerRidableInWater); ++ playerRemoveBindingWithWeakness = getBoolean("gameplay-mechanics.player.curse-of-binding.remove-with-weakness", playerRemoveBindingWithWeakness); ++ shiftRightClickRepairsMendingPoints = getInt("gameplay-mechanics.player.shift-right-click-repairs-mending-points", shiftRightClickRepairsMendingPoints); ++ playerExpPickupDelay = getInt("gameplay-mechanics.player.exp-pickup-delay-ticks", playerExpPickupDelay); ++ playerVoidTrading = getBoolean("gameplay-mechanics.player.allow-void-trading", playerVoidTrading); ++ } ++ ++ private static boolean projectileDespawnRateSettingsMigrated = false; ++ private void projectileDespawnRateSettings() { ++ if (PurpurConfig.version < 28 && !projectileDespawnRateSettingsMigrated) { ++ migrateProjectileDespawnRateSettings(EntityType.DRAGON_FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.EGG); ++ migrateProjectileDespawnRateSettings(EntityType.ENDER_PEARL); ++ migrateProjectileDespawnRateSettings(EntityType.EXPERIENCE_BOTTLE); ++ migrateProjectileDespawnRateSettings(EntityType.FIREWORK_ROCKET); ++ migrateProjectileDespawnRateSettings(EntityType.FISHING_BOBBER); ++ migrateProjectileDespawnRateSettings(EntityType.FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.LLAMA_SPIT); ++ migrateProjectileDespawnRateSettings(EntityType.POTION); ++ migrateProjectileDespawnRateSettings(EntityType.SHULKER_BULLET); ++ migrateProjectileDespawnRateSettings(EntityType.SMALL_FIREBALL); ++ migrateProjectileDespawnRateSettings(EntityType.SNOWBALL); ++ migrateProjectileDespawnRateSettings(EntityType.WITHER_SKULL); ++ //PufferfishConfig.save(); ++ set("gameplay-mechanics.projectile-despawn-rates", null); ++ // pufferfish's entity_timeout is a global config ++ // we only want to migrate values from the ++ // default world (first world loaded) ++ projectileDespawnRateSettingsMigrated = true; ++ } ++ } ++ private void migrateProjectileDespawnRateSettings(EntityType type) { ++ //String pufferName = "entity_timeouts." + type.id.toUpperCase(Locale.ROOT); ++ //int value = getInt("gameplay-mechanics.projectile-despawn-rates." + type.id, -1); ++ //if (value != -1 && PufferfishConfig.getRawInt(pufferName, -1) == -1) { ++ // PufferfishConfig.setInt(pufferName, value); ++ // type.ttl = value; ++ //} ++ } ++ ++ public double bowProjectileOffset = 1.0D; ++ public double crossbowProjectileOffset = 1.0D; ++ public double eggProjectileOffset = 1.0D; ++ public double enderPearlProjectileOffset = 1.0D; ++ public double throwablePotionProjectileOffset = 1.0D; ++ public double tridentProjectileOffset = 1.0D; ++ public double snowballProjectileOffset = 1.0D; ++ private void projectileOffsetSettings() { ++ bowProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.bow", bowProjectileOffset); ++ crossbowProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.crossbow", crossbowProjectileOffset); ++ eggProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.egg", eggProjectileOffset); ++ enderPearlProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.ender-pearl", enderPearlProjectileOffset); ++ throwablePotionProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.throwable-potion", throwablePotionProjectileOffset); ++ tridentProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.trident", tridentProjectileOffset); ++ snowballProjectileOffset = getDouble("gameplay-mechanics.projectile-offset.snowball", snowballProjectileOffset); ++ } ++ ++ public int snowballDamage = -1; ++ private void snowballSettings() { ++ snowballDamage = getInt("gameplay-mechanics.projectile-damage.snowball", snowballDamage); ++ } ++ ++ public List shovelTurnsBlockToGrassPath = new ArrayList<>(); ++ private void shovelSettings() { ++ getList("gameplay-mechanics.shovel-turns-block-to-grass-path", new ArrayList(){{ ++ add("minecraft:coarse_dirt"); ++ add("minecraft:dirt"); ++ add("minecraft:grass_block"); ++ add("minecraft:mycelium"); ++ add("minecraft:podzol"); ++ add("minecraft:rooted_dirt"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (block != Blocks.AIR) shovelTurnsBlockToGrassPath.add(block); ++ }); ++ } ++ ++ public boolean silkTouchEnabled = false; ++ public String silkTouchSpawnerName = "Monster Spawner"; ++ public List silkTouchSpawnerLore = new ArrayList<>(); ++ public List silkTouchTools = new ArrayList<>(); ++ public int minimumSilkTouchSpawnerRequire = 1; ++ private void silkTouchSettings() { ++ if (PurpurConfig.version < 21) { ++ String oldName = getString("gameplay-mechanics.silk-touch.spawner-name", silkTouchSpawnerName); ++ set("gameplay-mechanics.silk-touch.spawner-name", "" + ChatColor.toMM(oldName.replace("{mob}", ""))); ++ List list = new ArrayList<>(); ++ getList("gameplay-mechanics.silk-touch.spawner-lore", List.of("Spawns a ")) ++ .forEach(line -> list.add("" + ChatColor.toMM(line.toString().replace("{mob}", "")))); ++ set("gameplay-mechanics.silk-touch.spawner-lore", list); ++ } ++ silkTouchEnabled = getBoolean("gameplay-mechanics.silk-touch.enabled", silkTouchEnabled); ++ silkTouchSpawnerName = getString("gameplay-mechanics.silk-touch.spawner-name", silkTouchSpawnerName); ++ minimumSilkTouchSpawnerRequire = getInt("gameplay-mechanics.silk-touch.minimal-level", minimumSilkTouchSpawnerRequire); ++ silkTouchSpawnerLore.clear(); ++ getList("gameplay-mechanics.silk-touch.spawner-lore", List.of("Spawns a ")) ++ .forEach(line -> silkTouchSpawnerLore.add(line.toString())); ++ silkTouchTools.clear(); ++ getList("gameplay-mechanics.silk-touch.tools", List.of( ++ "minecraft:iron_pickaxe", ++ "minecraft:golden_pickaxe", ++ "minecraft:diamond_pickaxe", ++ "minecraft:netherite_pickaxe" ++ )).forEach(key -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(key.toString())); ++ if (item != Items.AIR) silkTouchTools.add(item); ++ }); ++ } ++ ++ public Map axeStrippables = new HashMap<>(); ++ public Map axeWaxables = new HashMap<>(); ++ public Map axeWeatherables = new HashMap<>(); ++ public Map hoeTillables = new HashMap<>(); ++ public boolean hoeReplantsCrops = false; ++ public boolean hoeReplantsNetherWarts = false; ++ private void toolSettings() { ++ axeStrippables.clear(); ++ axeWaxables.clear(); ++ axeWeatherables.clear(); ++ hoeTillables.clear(); ++ if (PurpurConfig.version < 18) { ++ ConfigurationSection section = PurpurConfig.config.getConfigurationSection("world-settings." + worldName + ".tools.hoe.tilling"); ++ if (section != null) { ++ PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tillables", section); ++ PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tilling", null); ++ } ++ section = PurpurConfig.config.getConfigurationSection("world-settings.default.tools.hoe.tilling"); ++ if (section != null) { ++ PurpurConfig.config.set("world-settings.default.tools.hoe.tillables", section); ++ PurpurConfig.config.set("world-settings.default.tools.hoe.tilling", null); ++ } ++ } ++ if (PurpurConfig.version < 29) { ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:mangrove_log", Map.of("into", "minecraft:stripped_mangrove_log", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:mangrove_wood", Map.of("into", "minecraft:stripped_mangrove_wood", "drops", new HashMap())); ++ } ++ if (PurpurConfig.version < 32) { ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:cherry_log", Map.of("into", "minecraft:stripped_cherry_log", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:cherry_wood", Map.of("into", "minecraft:stripped_cherry_wood", "drops", new HashMap())); ++ PurpurConfig.config.set("world-settings.default.tools.axe.strippables.minecraft:bamboo_block", Map.of("into", "minecraft:stripped_bamboo_block", "drops", new HashMap())); ++ } ++ getMap("tools.axe.strippables", Map.ofEntries( ++ Map.entry("minecraft:oak_wood", Map.of("into", "minecraft:stripped_oak_wood", "drops", new HashMap())), ++ Map.entry("minecraft:oak_log", Map.of("into", "minecraft:stripped_oak_log", "drops", new HashMap())), ++ Map.entry("minecraft:dark_oak_wood", Map.of("into", "minecraft:stripped_dark_oak_wood", "drops", new HashMap())), ++ Map.entry("minecraft:dark_oak_log", Map.of("into", "minecraft:stripped_dark_oak_log", "drops", new HashMap())), ++ Map.entry("minecraft:acacia_wood", Map.of("into", "minecraft:stripped_acacia_wood", "drops", new HashMap())), ++ Map.entry("minecraft:acacia_log", Map.of("into", "minecraft:stripped_acacia_log", "drops", new HashMap())), ++ Map.entry("minecraft:birch_wood", Map.of("into", "minecraft:stripped_birch_wood", "drops", new HashMap())), ++ Map.entry("minecraft:birch_log", Map.of("into", "minecraft:stripped_birch_log", "drops", new HashMap())), ++ Map.entry("minecraft:jungle_wood", Map.of("into", "minecraft:stripped_jungle_wood", "drops", new HashMap())), ++ Map.entry("minecraft:jungle_log", Map.of("into", "minecraft:stripped_jungle_log", "drops", new HashMap())), ++ Map.entry("minecraft:spruce_wood", Map.of("into", "minecraft:stripped_spruce_wood", "drops", new HashMap())), ++ Map.entry("minecraft:spruce_log", Map.of("into", "minecraft:stripped_spruce_log", "drops", new HashMap())), ++ Map.entry("minecraft:mangrove_wood", Map.of("into", "minecraft:stripped_mangrove_wood", "drops", new HashMap())), ++ Map.entry("minecraft:mangrove_log", Map.of("into", "minecraft:stripped_mangrove_log", "drops", new HashMap())), ++ Map.entry("minecraft:cherry_log", Map.of("into", "minecraft:stripped_cherry_log", "drops", new HashMap())), ++ Map.entry("minecraft:cherry_wood", Map.of("into", "minecraft:stripped_cherry_wood", "drops", new HashMap())), ++ Map.entry("minecraft:bamboo_block", Map.of("into", "minecraft:stripped_bamboo_block", "drops", new HashMap())), ++ Map.entry("minecraft:warped_stem", Map.of("into", "minecraft:stripped_warped_stem", "drops", new HashMap())), ++ Map.entry("minecraft:warped_hyphae", Map.of("into", "minecraft:stripped_warped_hyphae", "drops", new HashMap())), ++ Map.entry("minecraft:crimson_stem", Map.of("into", "minecraft:stripped_crimson_stem", "drops", new HashMap())), ++ Map.entry("minecraft:crimson_hyphae", Map.of("into", "minecraft:stripped_crimson_hyphae", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.strippables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeStrippables.put(block, new Strippable(into, drops)); ++ }); ++ getMap("tools.axe.waxables", Map.ofEntries( ++ Map.entry("minecraft:waxed_copper_block", Map.of("into", "minecraft:copper_block", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_copper", Map.of("into", "minecraft:oxidized_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper", Map.of("into", "minecraft:oxidized_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper_slab", Map.of("into", "minecraft:oxidized_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_exposed_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_weathered_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:waxed_oxidized_cut_copper_stairs", Map.of("into", "minecraft:oxidized_cut_copper_stairs", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.waxables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeWaxables.put(block, new Waxable(into, drops)); ++ }); ++ getMap("tools.axe.weatherables", Map.ofEntries( ++ Map.entry("minecraft:exposed_copper", Map.of("into", "minecraft:copper_block", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap())), ++ Map.entry("minecraft:exposed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:weathered_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap())), ++ Map.entry("minecraft:oxidized_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap()))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + "`"); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.weatherables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ axeWeatherables.put(block, new Weatherable(into, drops)); ++ }); ++ getMap("tools.hoe.tillables", Map.ofEntries( ++ Map.entry("minecraft:grass_block", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:dirt_path", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:dirt", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap())), ++ Map.entry("minecraft:coarse_dirt", Map.of("condition", "air_above", "into", "minecraft:dirt", "drops", new HashMap())), ++ Map.entry("minecraft:rooted_dirt", Map.of("condition", "always", "into", "minecraft:dirt", "drops", Map.of("minecraft:hanging_roots", 1.0D)))) ++ ).forEach((blockId, obj) -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(blockId)); ++ if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tillables`: " + blockId); return; } ++ if (!(obj instanceof Map map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tillables." + blockId + "`"); return; } ++ String conditionId = (String) map.get("condition"); ++ Tillable.Condition condition = Tillable.Condition.get(conditionId); ++ if (condition == null) { PurpurConfig.log(Level.SEVERE, "Invalid condition for `tools.hoe.tillables." + blockId + ".condition`: " + conditionId); return; } ++ String intoId = (String) map.get("into"); ++ Block into = BuiltInRegistries.BLOCK.get(new ResourceLocation(intoId)); ++ if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tillables." + blockId + ".into`: " + intoId); return; } ++ Object dropsObj = map.get("drops"); ++ if (!(dropsObj instanceof Map dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tillables." + blockId + ".drops`"); return; } ++ Map drops = new HashMap<>(); ++ dropsMap.forEach((itemId, chance) -> { ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId.toString())); ++ if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.hoe.tillables." + blockId + ".drops`: " + itemId); return; } ++ drops.put(item, (double) chance); ++ }); ++ hoeTillables.put(block, new Tillable(condition, into, drops)); ++ }); ++ hoeReplantsCrops = getBoolean("tools.hoe.replant-crops", hoeReplantsCrops); ++ hoeReplantsNetherWarts = getBoolean("tools.hoe.replant-nether-warts", hoeReplantsNetherWarts); ++ } ++ ++ public boolean anvilAllowColors = false; ++ public boolean anvilColorsUseMiniMessage; ++ public int anvilRepairIngotsAmount = 0; ++ public int anvilDamageObsidianAmount = 0; ++ private void anvilSettings() { ++ anvilAllowColors = getBoolean("blocks.anvil.allow-colors", anvilAllowColors); ++ anvilColorsUseMiniMessage = getBoolean("blocks.anvil.use-mini-message", anvilColorsUseMiniMessage); ++ anvilRepairIngotsAmount = getInt("blocks.anvil.iron-ingots-used-for-repair", anvilRepairIngotsAmount); ++ anvilDamageObsidianAmount = getInt("blocks.anvil.obsidian-used-for-damage", anvilDamageObsidianAmount); ++ } ++ ++ public double azaleaGrowthChance = 0.0D; ++ private void azaleaSettings() { ++ azaleaGrowthChance = getDouble("blocks.azalea.growth-chance", azaleaGrowthChance); ++ } ++ ++ public int beaconLevelOne = 20; ++ public int beaconLevelTwo = 30; ++ public int beaconLevelThree = 40; ++ public int beaconLevelFour = 50; ++ public boolean beaconAllowEffectsWithTintedGlass = false; ++ private void beaconSettings() { ++ beaconLevelOne = getInt("blocks.beacon.effect-range.level-1", beaconLevelOne); ++ beaconLevelTwo = getInt("blocks.beacon.effect-range.level-2", beaconLevelTwo); ++ beaconLevelThree = getInt("blocks.beacon.effect-range.level-3", beaconLevelThree); ++ beaconLevelFour = getInt("blocks.beacon.effect-range.level-4", beaconLevelFour); ++ beaconAllowEffectsWithTintedGlass = getBoolean("blocks.beacon.allow-effects-with-tinted-glass", beaconAllowEffectsWithTintedGlass); ++ } ++ ++ public boolean bedExplode = true; ++ public boolean bedExplodeOnVillagerSleep = false; ++ public double bedExplosionPower = 5.0D; ++ public boolean bedExplosionFire = true; ++ public net.minecraft.world.level.Level.ExplosionInteraction bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ private void bedSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.bed.explosion-effect", bedExplosionEffect.name()))) { ++ set("blocks.bed.explosion-effect", "BLOCK"); ++ } ++ } ++ bedExplode = getBoolean("blocks.bed.explode", bedExplode); ++ bedExplodeOnVillagerSleep = getBoolean("blocks.bed.explode-on-villager-sleep", bedExplodeOnVillagerSleep); ++ bedExplosionPower = getDouble("blocks.bed.explosion-power", bedExplosionPower); ++ bedExplosionFire = getBoolean("blocks.bed.explosion-fire", bedExplosionFire); ++ try { ++ bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.bed.explosion-effect", bedExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.bed.explosion-effect`! Using default of `BLOCK`"); ++ bedExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ } ++ ++ public Map bigDripleafTiltDelay = new HashMap<>(); ++ private void bigDripleafSettings() { ++ bigDripleafTiltDelay.clear(); ++ getMap("blocks.big_dripleaf.tilt-delay", Map.ofEntries( ++ Map.entry("UNSTABLE", 10), ++ Map.entry("PARTIAL", 10), ++ Map.entry("FULL", 100)) ++ ).forEach((tilt, delay) -> { ++ try { ++ bigDripleafTiltDelay.put(Tilt.valueOf(tilt), (int) delay); ++ } catch (IllegalArgumentException e) { ++ PurpurConfig.log(Level.SEVERE, "Invalid big_dripleaf tilt key: " + tilt); ++ } ++ }); ++ } ++ ++ public boolean buddingAmethystSilkTouch = false; ++ private void buddingAmethystSettings() { ++ buddingAmethystSilkTouch = getBoolean("blocks.budding_amethyst.silk-touch", buddingAmethystSilkTouch); ++ } ++ ++ public boolean cactusBreaksFromSolidNeighbors = true; ++ public boolean cactusAffectedByBonemeal = false; ++ private void cactusSettings() { ++ cactusBreaksFromSolidNeighbors = getBoolean("blocks.cactus.breaks-from-solid-neighbors", cactusBreaksFromSolidNeighbors); ++ cactusAffectedByBonemeal = getBoolean("blocks.cactus.affected-by-bonemeal", cactusAffectedByBonemeal); ++ } ++ ++ public boolean sugarCanAffectedByBonemeal = false; ++ private void sugarCaneSettings() { ++ sugarCanAffectedByBonemeal = getBoolean("blocks.sugar_cane.affected-by-bonemeal", sugarCanAffectedByBonemeal); ++ } ++ ++ public boolean netherWartAffectedByBonemeal = false; ++ private void netherWartSettings() { ++ netherWartAffectedByBonemeal = getBoolean("blocks.nether_wart.affected-by-bonemeal", netherWartAffectedByBonemeal); ++ } ++ ++ public boolean campFireLitWhenPlaced = true; ++ private void campFireSettings() { ++ campFireLitWhenPlaced = getBoolean("blocks.campfire.lit-when-placed", campFireLitWhenPlaced); ++ } ++ ++ public boolean chestOpenWithBlockOnTop = false; ++ private void chestSettings() { ++ chestOpenWithBlockOnTop = getBoolean("blocks.chest.open-with-solid-block-on-top", chestOpenWithBlockOnTop); ++ } ++ ++ public boolean composterBulkProcess = false; ++ private void composterSettings() { ++ composterBulkProcess = getBoolean("blocks.composter.sneak-to-bulk-process", composterBulkProcess); ++ } ++ ++ public boolean coralDieOutsideWater = true; ++ private void coralSettings() { ++ coralDieOutsideWater = getBoolean("blocks.coral.die-outside-water", coralDieOutsideWater); ++ } ++ ++ public boolean dispenserApplyCursedArmor = true; ++ public boolean dispenserPlaceAnvils = false; ++ private void dispenserSettings() { ++ dispenserApplyCursedArmor = getBoolean("blocks.dispenser.apply-cursed-to-armor-slots", dispenserApplyCursedArmor); ++ dispenserPlaceAnvils = getBoolean("blocks.dispenser.place-anvils", dispenserPlaceAnvils); ++ } ++ ++ public List doorRequiresRedstone = new ArrayList<>(); ++ private void doorSettings() { ++ getList("blocks.door.requires-redstone", new ArrayList()).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ doorRequiresRedstone.add(block); ++ } ++ }); ++ } ++ ++ public boolean dragonEggTeleport = true; ++ private void dragonEggSettings() { ++ dragonEggTeleport = getBoolean("blocks.dragon_egg.teleport", dragonEggTeleport); ++ } ++ ++ public boolean baselessEndCrystalExplode = true; ++ public double baselessEndCrystalExplosionPower = 6.0D; ++ public boolean baselessEndCrystalExplosionFire = false; ++ public net.minecraft.world.level.Level.ExplosionInteraction baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ public boolean basedEndCrystalExplode = true; ++ public double basedEndCrystalExplosionPower = 6.0D; ++ public boolean basedEndCrystalExplosionFire = false; ++ public net.minecraft.world.level.Level.ExplosionInteraction basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ public int endCrystalCramming = 0; ++ private void endCrystalSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.end-crystal.baseless.explosion-effect", baselessEndCrystalExplosionEffect.name()))) { ++ set("blocks.end-crystal.baseless.explosion-effect", "BLOCK"); ++ } ++ if ("DESTROY".equals(getString("blocks.end-crystal.base.explosion-effect", basedEndCrystalExplosionEffect.name()))) { ++ set("blocks.end-crystal.base.explosion-effect", "BLOCK"); ++ } ++ } ++ baselessEndCrystalExplode = getBoolean("blocks.end-crystal.baseless.explode", baselessEndCrystalExplode); ++ baselessEndCrystalExplosionPower = getDouble("blocks.end-crystal.baseless.explosion-power", baselessEndCrystalExplosionPower); ++ baselessEndCrystalExplosionFire = getBoolean("blocks.end-crystal.baseless.explosion-fire", baselessEndCrystalExplosionFire); ++ try { ++ baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.end-crystal.baseless.explosion-effect", baselessEndCrystalExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.end-crystal.baseless.explosion-effect`! Using default of `BLOCK`"); ++ baselessEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ basedEndCrystalExplode = getBoolean("blocks.end-crystal.base.explode", basedEndCrystalExplode); ++ basedEndCrystalExplosionPower = getDouble("blocks.end-crystal.base.explosion-power", basedEndCrystalExplosionPower); ++ basedEndCrystalExplosionFire = getBoolean("blocks.end-crystal.base.explosion-fire", basedEndCrystalExplosionFire); ++ try { ++ basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.end-crystal.base.explosion-effect", basedEndCrystalExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.end-crystal.base.explosion-effect`! Using default of `BLOCK`"); ++ basedEndCrystalExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ endCrystalCramming = getInt("blocks.end-crystal.cramming-amount", endCrystalCramming); ++ } ++ ++ public boolean farmlandBypassMobGriefing = false; ++ public boolean farmlandGetsMoistFromBelow = false; ++ public boolean farmlandAlpha = false; ++ public boolean farmlandTramplingDisabled = false; ++ public boolean farmlandTramplingOnlyPlayers = false; ++ public boolean farmlandTramplingFeatherFalling = false; ++ public double farmlandTrampleHeight = -1D; ++ private void farmlandSettings() { ++ farmlandBypassMobGriefing = getBoolean("blocks.farmland.bypass-mob-griefing", farmlandBypassMobGriefing); ++ farmlandGetsMoistFromBelow = getBoolean("blocks.farmland.gets-moist-from-below", farmlandGetsMoistFromBelow); ++ farmlandAlpha = getBoolean("blocks.farmland.use-alpha-farmland", farmlandAlpha); ++ farmlandTramplingDisabled = getBoolean("blocks.farmland.disable-trampling", farmlandTramplingDisabled); ++ farmlandTramplingOnlyPlayers = getBoolean("blocks.farmland.only-players-trample", farmlandTramplingOnlyPlayers); ++ farmlandTramplingFeatherFalling = getBoolean("blocks.farmland.feather-fall-distance-affects-trampling", farmlandTramplingFeatherFalling); ++ farmlandTrampleHeight = getDouble("blocks.farmland.trample-height", farmlandTrampleHeight); ++ } ++ ++ public double floweringAzaleaGrowthChance = 0.0D; ++ private void floweringAzaleaSettings() { ++ floweringAzaleaGrowthChance = getDouble("blocks.flowering_azalea.growth-chance", floweringAzaleaGrowthChance); ++ } ++ ++ public boolean furnaceUseLavaFromUnderneath = false; ++ private void furnaceSettings() { ++ if (PurpurConfig.version < 17) { ++ furnaceUseLavaFromUnderneath = getBoolean("blocks.furnace.infinite-fuel", furnaceUseLavaFromUnderneath); ++ boolean oldValue = getBoolean("blocks.furnace.infinite-fuel", furnaceUseLavaFromUnderneath); ++ set("blocks.furnace.infinite-fuel", null); ++ set("blocks.furnace.use-lava-from-underneath", oldValue); ++ } ++ furnaceUseLavaFromUnderneath = getBoolean("blocks.furnace.use-lava-from-underneath", furnaceUseLavaFromUnderneath); ++ } ++ ++ public boolean endPortalSafeTeleporting = true; ++ private void endPortalSettings() { ++ endPortalSafeTeleporting = getBoolean("blocks.end_portal.safe-teleporting", endPortalSafeTeleporting); ++ } ++ ++ public boolean mobsSpawnOnPackedIce = true; ++ public boolean mobsSpawnOnBlueIce = true; ++ public boolean snowOnBlueIce = true; ++ private void iceSettings() { ++ mobsSpawnOnPackedIce = getBoolean("blocks.packed_ice.allow-mob-spawns", mobsSpawnOnPackedIce); ++ mobsSpawnOnBlueIce = getBoolean("blocks.blue_ice.allow-mob-spawns", mobsSpawnOnBlueIce); ++ snowOnBlueIce = getBoolean("blocks.blue_ice.allow-snow-formation", snowOnBlueIce); ++ } ++ ++ public int lavaInfiniteRequiredSources = 2; ++ public int lavaSpeedNether = 10; ++ public int lavaSpeedNotNether = 30; ++ private void lavaSettings() { ++ lavaInfiniteRequiredSources = getInt("blocks.lava.infinite-required-sources", lavaInfiniteRequiredSources); ++ lavaSpeedNether = getInt("blocks.lava.speed.nether", lavaSpeedNether); ++ lavaSpeedNotNether = getInt("blocks.lava.speed.not-nether", lavaSpeedNotNether); ++ } ++ ++ public int pistonBlockPushLimit = 12; ++ private void pistonSettings() { ++ pistonBlockPushLimit = getInt("blocks.piston.block-push-limit", pistonBlockPushLimit); ++ } ++ ++ public boolean magmaBlockDamageWhenSneaking = false; ++ public boolean magmaBlockDamageWithFrostWalker = false; ++ private void magmaBlockSettings() { ++ magmaBlockDamageWhenSneaking = getBoolean("blocks.magma-block.damage-when-sneaking", magmaBlockDamageWhenSneaking); ++ magmaBlockDamageWithFrostWalker = getBoolean("blocks.magma-block.damage-with-frost-walker", magmaBlockDamageWithFrostWalker); ++ } ++ ++ public boolean powderSnowBypassMobGriefing = false; ++ private void powderSnowSettings() { ++ powderSnowBypassMobGriefing = getBoolean("blocks.powder_snow.bypass-mob-griefing", powderSnowBypassMobGriefing); ++ } ++ ++ public int railActivationRange = 8; ++ private void railSettings() { ++ railActivationRange = getInt("blocks.powered-rail.activation-range", railActivationRange); ++ } ++ ++ public boolean respawnAnchorExplode = true; ++ public double respawnAnchorExplosionPower = 5.0D; ++ public boolean respawnAnchorExplosionFire = true; ++ public net.minecraft.world.level.Level.ExplosionInteraction respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ private void respawnAnchorSettings() { ++ if (PurpurConfig.version < 31) { ++ if ("DESTROY".equals(getString("blocks.respawn_anchor.explosion-effect", respawnAnchorExplosionEffect.name()))) { ++ set("blocks.respawn_anchor.explosion-effect", "BLOCK"); ++ } ++ } ++ respawnAnchorExplode = getBoolean("blocks.respawn_anchor.explode", respawnAnchorExplode); ++ respawnAnchorExplosionPower = getDouble("blocks.respawn_anchor.explosion-power", respawnAnchorExplosionPower); ++ respawnAnchorExplosionFire = getBoolean("blocks.respawn_anchor.explosion-fire", respawnAnchorExplosionFire); ++ try { ++ respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.valueOf(getString("blocks.respawn_anchor.explosion-effect", respawnAnchorExplosionEffect.name())); ++ } catch (IllegalArgumentException e) { ++ log(Level.SEVERE, "Unknown value for `blocks.respawn_anchor.explosion-effect`! Using default of `BLOCK`"); ++ respawnAnchorExplosionEffect = net.minecraft.world.level.Level.ExplosionInteraction.BLOCK; ++ } ++ } ++ ++ public boolean fixSandDuping = true; ++ private void sandSettings() { ++ fixSandDuping = getBoolean("blocks.sand.fix-duping", fixSandDuping); ++ } ++ ++ public boolean sculkShriekerCanSummonDefault = false; ++ private void sculkShriekerSettings() { ++ sculkShriekerCanSummonDefault = getBoolean("blocks.sculk_shrieker.can-summon-default", sculkShriekerCanSummonDefault); ++ } ++ ++ public boolean shulkerBoxAllowOversizedStacks = false; ++ private void shulkerBoxSettings() { ++ shulkerBoxAllowOversizedStacks = getBoolean("blocks.shulker_box.allow-oversized-stacks", shulkerBoxAllowOversizedStacks); ++ } ++ ++ public boolean signRightClickEdit = false; ++ public boolean signAllowColors = false; ++ private void signSettings() { ++ signRightClickEdit = getBoolean("blocks.sign.right-click-edit", signRightClickEdit); ++ signAllowColors = getBoolean("blocks.sign.allow-colors", signAllowColors); ++ } ++ ++ public boolean slabHalfBreak = false; ++ private void slabSettings() { ++ slabHalfBreak = getBoolean("blocks.slab.break-individual-slabs-when-sneaking", slabHalfBreak); ++ } ++ ++ public boolean spawnerDeactivateByRedstone = false; ++ public boolean spawnerFixMC238526 = false; ++ private void spawnerSettings() { ++ spawnerDeactivateByRedstone = getBoolean("blocks.spawner.deactivate-by-redstone", spawnerDeactivateByRedstone); ++ spawnerFixMC238526 = getBoolean("blocks.spawner.fix-mc-238526", spawnerFixMC238526); ++ } ++ ++ public int spongeAbsorptionArea = 64; ++ public int spongeAbsorptionRadius = 6; ++ public boolean spongeAbsorbsLava = false; ++ private void spongeSettings() { ++ spongeAbsorptionArea = getInt("blocks.sponge.absorption.area", spongeAbsorptionArea); ++ spongeAbsorptionRadius = getInt("blocks.sponge.absorption.radius", spongeAbsorptionRadius); ++ spongeAbsorbsLava = getBoolean("blocks.sponge.absorbs-lava", spongeAbsorbsLava); ++ } ++ ++ public float stonecutterDamage = 0.0F; ++ private void stonecutterSettings() { ++ stonecutterDamage = (float) getDouble("blocks.stonecutter.damage", stonecutterDamage); ++ } ++ ++ public boolean turtleEggsBreakFromExpOrbs = true; ++ public boolean turtleEggsBreakFromItems = true; ++ public boolean turtleEggsBreakFromMinecarts = true; ++ public boolean turtleEggsBypassMobGriefing = false; ++ public int turtleEggsRandomTickCrackChance = 500; ++ public boolean turtleEggsTramplingFeatherFalling = false; ++ private void turtleEggSettings() { ++ turtleEggsBreakFromExpOrbs = getBoolean("blocks.turtle_egg.break-from-exp-orbs", turtleEggsBreakFromExpOrbs); ++ turtleEggsBreakFromItems = getBoolean("blocks.turtle_egg.break-from-items", turtleEggsBreakFromItems); ++ turtleEggsBreakFromMinecarts = getBoolean("blocks.turtle_egg.break-from-minecarts", turtleEggsBreakFromMinecarts); ++ turtleEggsBypassMobGriefing = getBoolean("blocks.turtle_egg.bypass-mob-griefing", turtleEggsBypassMobGriefing); ++ turtleEggsRandomTickCrackChance = getInt("blocks.turtle_egg.random-tick-crack-chance", turtleEggsRandomTickCrackChance); ++ turtleEggsTramplingFeatherFalling = getBoolean("blocks.turtle_egg.feather-fall-distance-affects-trampling", turtleEggsTramplingFeatherFalling); ++ } ++ ++ public int waterInfiniteRequiredSources = 2; ++ private void waterSources() { ++ waterInfiniteRequiredSources = getInt("blocks.water.infinite-required-sources", waterInfiniteRequiredSources); ++ } ++ ++ public boolean babiesAreRidable = true; ++ public boolean untamedTamablesAreRidable = true; ++ public boolean useNightVisionWhenRiding = false; ++ public boolean useDismountsUnderwaterTag = true; ++ private void ridableSettings() { ++ babiesAreRidable = getBoolean("ridable-settings.babies-are-ridable", babiesAreRidable); ++ untamedTamablesAreRidable = getBoolean("ridable-settings.untamed-tamables-are-ridable", untamedTamablesAreRidable); ++ useNightVisionWhenRiding = getBoolean("ridable-settings.use-night-vision", useNightVisionWhenRiding); ++ useDismountsUnderwaterTag = getBoolean("ridable-settings.use-dismounts-underwater-tag", useDismountsUnderwaterTag); ++ } ++ ++ public boolean allayRidable = false; ++ public boolean allayRidableInWater = true; ++ public boolean allayControllable = true; ++ public List allayRespectNBT = new ArrayList<>(); ++ private void allaySettings() { ++ allayRidable = getBoolean("mobs.allay.ridable", allayRidable); ++ allayRidableInWater = getBoolean("mobs.allay.ridable-in-water", allayRidableInWater); ++ allayControllable = getBoolean("mobs.allay.controllable", allayControllable); ++ allayRespectNBT.clear(); ++ getList("mobs.allay.respect-nbt", new ArrayList<>()).forEach(key -> allayRespectNBT.add(key.toString())); ++ } ++ ++ public boolean axolotlRidable = false; ++ public boolean axolotlControllable = true; ++ public double axolotlMaxHealth = 14.0D; ++ public int axolotlBreedingTicks = 6000; ++ public boolean axolotlTakeDamageFromWater = false; ++ public boolean axolotlAlwaysDropExp = false; ++ private void axolotlSettings() { ++ axolotlRidable = getBoolean("mobs.axolotl.ridable", axolotlRidable); ++ axolotlControllable = getBoolean("mobs.axolotl.controllable", axolotlControllable); ++ axolotlMaxHealth = getDouble("mobs.axolotl.attributes.max_health", axolotlMaxHealth); ++ axolotlBreedingTicks = getInt("mobs.axolotl.breeding-delay-ticks", axolotlBreedingTicks); ++ axolotlTakeDamageFromWater = getBoolean("mobs.axolotl.takes-damage-from-water", axolotlTakeDamageFromWater); ++ axolotlAlwaysDropExp = getBoolean("mobs.axolotl.always-drop-exp", axolotlAlwaysDropExp); ++ } ++ ++ public boolean batRidable = false; ++ public boolean batRidableInWater = true; ++ public boolean batControllable = true; ++ public double batMaxY = 320D; ++ public double batMaxHealth = 6.0D; ++ public double batFollowRange = 16.0D; ++ public double batKnockbackResistance = 0.0D; ++ public double batMovementSpeed = 0.6D; ++ public double batFlyingSpeed = 0.6D; ++ public double batArmor = 0.0D; ++ public double batArmorToughness = 0.0D; ++ public double batAttackKnockback = 0.0D; ++ public boolean batTakeDamageFromWater = false; ++ public boolean batAlwaysDropExp = false; ++ private void batSettings() { ++ batRidable = getBoolean("mobs.bat.ridable", batRidable); ++ batRidableInWater = getBoolean("mobs.bat.ridable-in-water", batRidableInWater); ++ batControllable = getBoolean("mobs.bat.controllable", batControllable); ++ batMaxY = getDouble("mobs.bat.ridable-max-y", batMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.bat.attributes.max-health", batMaxHealth); ++ set("mobs.bat.attributes.max-health", null); ++ set("mobs.bat.attributes.max_health", oldValue); ++ } ++ batMaxHealth = getDouble("mobs.bat.attributes.max_health", batMaxHealth); ++ batTakeDamageFromWater = getBoolean("mobs.bat.takes-damage-from-water", batTakeDamageFromWater); ++ batAlwaysDropExp = getBoolean("mobs.bat.always-drop-exp", batAlwaysDropExp); ++ } ++ ++ public boolean beeRidable = false; ++ public boolean beeRidableInWater = true; ++ public boolean beeControllable = true; ++ public double beeMaxY = 320D; ++ public double beeMaxHealth = 10.0D; ++ public int beeBreedingTicks = 6000; ++ public boolean beeTakeDamageFromWater = false; ++ public boolean beeCanWorkAtNight = false; ++ public boolean beeCanWorkInRain = false; ++ public boolean beeAlwaysDropExp = false; ++ public boolean beeDiesAfterSting = true; ++ private void beeSettings() { ++ beeRidable = getBoolean("mobs.bee.ridable", beeRidable); ++ beeRidableInWater = getBoolean("mobs.bee.ridable-in-water", beeRidableInWater); ++ beeControllable = getBoolean("mobs.bee.controllable", beeControllable); ++ beeMaxY = getDouble("mobs.bee.ridable-max-y", beeMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.bee.attributes.max-health", beeMaxHealth); ++ set("mobs.bee.attributes.max-health", null); ++ set("mobs.bee.attributes.max_health", oldValue); ++ } ++ beeMaxHealth = getDouble("mobs.bee.attributes.max_health", beeMaxHealth); ++ beeBreedingTicks = getInt("mobs.bee.breeding-delay-ticks", beeBreedingTicks); ++ beeTakeDamageFromWater = getBoolean("mobs.bee.takes-damage-from-water", beeTakeDamageFromWater); ++ beeCanWorkAtNight = getBoolean("mobs.bee.can-work-at-night", beeCanWorkAtNight); ++ beeCanWorkInRain = getBoolean("mobs.bee.can-work-in-rain", beeCanWorkInRain); ++ beeAlwaysDropExp = getBoolean("mobs.bee.always-drop-exp", beeAlwaysDropExp); ++ beeDiesAfterSting = getBoolean("mobs.bee.dies-after-sting", beeDiesAfterSting); ++ } ++ ++ public boolean blazeRidable = false; ++ public boolean blazeRidableInWater = true; ++ public boolean blazeControllable = true; ++ public double blazeMaxY = 320D; ++ public double blazeMaxHealth = 20.0D; ++ public boolean blazeTakeDamageFromWater = true; ++ public boolean blazeAlwaysDropExp = false; ++ private void blazeSettings() { ++ blazeRidable = getBoolean("mobs.blaze.ridable", blazeRidable); ++ blazeRidableInWater = getBoolean("mobs.blaze.ridable-in-water", blazeRidableInWater); ++ blazeControllable = getBoolean("mobs.blaze.controllable", blazeControllable); ++ blazeMaxY = getDouble("mobs.blaze.ridable-max-y", blazeMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.blaze.attributes.max-health", blazeMaxHealth); ++ set("mobs.blaze.attributes.max-health", null); ++ set("mobs.blaze.attributes.max_health", oldValue); ++ } ++ blazeMaxHealth = getDouble("mobs.blaze.attributes.max_health", blazeMaxHealth); ++ blazeTakeDamageFromWater = getBoolean("mobs.blaze.takes-damage-from-water", blazeTakeDamageFromWater); ++ blazeAlwaysDropExp = getBoolean("mobs.blaze.always-drop-exp", blazeAlwaysDropExp); ++ } ++ ++ public int camelBreedingTicks = 6000; ++ public double camelMaxHealthMin = 32.0D; ++ public double camelMaxHealthMax = 32.0D; ++ public double camelJumpStrengthMin = 0.42D; ++ public double camelJumpStrengthMax = 0.42D; ++ public double camelMovementSpeedMin = 0.09D; ++ public double camelMovementSpeedMax = 0.09D; ++ private void camelSettings() { ++ camelMaxHealthMin = getDouble("mobs.camel.attributes.max_health.min", camelMaxHealthMin); ++ camelMaxHealthMax = getDouble("mobs.camel.attributes.max_health.max", camelMaxHealthMax); ++ camelJumpStrengthMin = getDouble("mobs.camel.attributes.jump_strength.min", camelJumpStrengthMin); ++ camelJumpStrengthMax = getDouble("mobs.camel.attributes.jump_strength.max", camelJumpStrengthMax); ++ camelMovementSpeedMin = getDouble("mobs.camel.attributes.movement_speed.min", camelMovementSpeedMin); ++ camelMovementSpeedMax = getDouble("mobs.camel.attributes.movement_speed.max", camelMovementSpeedMax); ++ camelBreedingTicks = getInt("mobs.camel.breeding-delay-ticks", camelBreedingTicks); ++ } ++ ++ public boolean catRidable = false; ++ public boolean catRidableInWater = true; ++ public boolean catControllable = true; ++ public double catMaxHealth = 10.0D; ++ public int catSpawnDelay = 1200; ++ public int catSpawnSwampHutScanRange = 16; ++ public int catSpawnVillageScanRange = 48; ++ public int catBreedingTicks = 6000; ++ public DyeColor catDefaultCollarColor = DyeColor.RED; ++ public boolean catTakeDamageFromWater = false; ++ public boolean catAlwaysDropExp = false; ++ private void catSettings() { ++ catRidable = getBoolean("mobs.cat.ridable", catRidable); ++ catRidableInWater = getBoolean("mobs.cat.ridable-in-water", catRidableInWater); ++ catControllable = getBoolean("mobs.cat.controllable", catControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cat.attributes.max-health", catMaxHealth); ++ set("mobs.cat.attributes.max-health", null); ++ set("mobs.cat.attributes.max_health", oldValue); ++ } ++ catMaxHealth = getDouble("mobs.cat.attributes.max_health", catMaxHealth); ++ catSpawnDelay = getInt("mobs.cat.spawn-delay", catSpawnDelay); ++ catSpawnSwampHutScanRange = getInt("mobs.cat.scan-range-for-other-cats.swamp-hut", catSpawnSwampHutScanRange); ++ catSpawnVillageScanRange = getInt("mobs.cat.scan-range-for-other-cats.village", catSpawnVillageScanRange); ++ catBreedingTicks = getInt("mobs.cat.breeding-delay-ticks", catBreedingTicks); ++ try { ++ catDefaultCollarColor = DyeColor.valueOf(getString("mobs.cat.default-collar-color", wolfDefaultCollarColor.name())); ++ } catch (IllegalArgumentException ignore) { ++ catDefaultCollarColor = DyeColor.RED; ++ } ++ catTakeDamageFromWater = getBoolean("mobs.cat.takes-damage-from-water", catTakeDamageFromWater); ++ catAlwaysDropExp = getBoolean("mobs.cat.always-drop-exp", catAlwaysDropExp); ++ } ++ ++ public boolean caveSpiderRidable = false; ++ public boolean caveSpiderRidableInWater = true; ++ public boolean caveSpiderControllable = true; ++ public double caveSpiderMaxHealth = 12.0D; ++ public boolean caveSpiderTakeDamageFromWater = false; ++ public boolean caveSpiderAlwaysDropExp = false; ++ private void caveSpiderSettings() { ++ caveSpiderRidable = getBoolean("mobs.cave_spider.ridable", caveSpiderRidable); ++ caveSpiderRidableInWater = getBoolean("mobs.cave_spider.ridable-in-water", caveSpiderRidableInWater); ++ caveSpiderControllable = getBoolean("mobs.cave_spider.controllable", caveSpiderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cave_spider.attributes.max-health", caveSpiderMaxHealth); ++ set("mobs.cave_spider.attributes.max-health", null); ++ set("mobs.cave_spider.attributes.max_health", oldValue); ++ } ++ caveSpiderMaxHealth = getDouble("mobs.cave_spider.attributes.max_health", caveSpiderMaxHealth); ++ caveSpiderTakeDamageFromWater = getBoolean("mobs.cave_spider.takes-damage-from-water", caveSpiderTakeDamageFromWater); ++ caveSpiderAlwaysDropExp = getBoolean("mobs.cave_spider.always-drop-exp", caveSpiderAlwaysDropExp); ++ } ++ ++ public boolean chickenRidable = false; ++ public boolean chickenRidableInWater = false; ++ public boolean chickenControllable = true; ++ public double chickenMaxHealth = 4.0D; ++ public boolean chickenRetaliate = false; ++ public int chickenBreedingTicks = 6000; ++ public boolean chickenTakeDamageFromWater = false; ++ public boolean chickenAlwaysDropExp = false; ++ private void chickenSettings() { ++ chickenRidable = getBoolean("mobs.chicken.ridable", chickenRidable); ++ chickenRidableInWater = getBoolean("mobs.chicken.ridable-in-water", chickenRidableInWater); ++ chickenControllable = getBoolean("mobs.chicken.controllable", chickenControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.chicken.attributes.max-health", chickenMaxHealth); ++ set("mobs.chicken.attributes.max-health", null); ++ set("mobs.chicken.attributes.max_health", oldValue); ++ } ++ chickenMaxHealth = getDouble("mobs.chicken.attributes.max_health", chickenMaxHealth); ++ chickenRetaliate = getBoolean("mobs.chicken.retaliate", chickenRetaliate); ++ chickenBreedingTicks = getInt("mobs.chicken.breeding-delay-ticks", chickenBreedingTicks); ++ chickenTakeDamageFromWater = getBoolean("mobs.chicken.takes-damage-from-water", chickenTakeDamageFromWater); ++ chickenAlwaysDropExp = getBoolean("mobs.chicken.always-drop-exp", chickenAlwaysDropExp); ++ } ++ ++ public boolean codRidable = false; ++ public boolean codControllable = true; ++ public double codMaxHealth = 3.0D; ++ public boolean codTakeDamageFromWater = false; ++ public boolean codAlwaysDropExp = false; ++ private void codSettings() { ++ codRidable = getBoolean("mobs.cod.ridable", codRidable); ++ codControllable = getBoolean("mobs.cod.controllable", codControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cod.attributes.max-health", codMaxHealth); ++ set("mobs.cod.attributes.max-health", null); ++ set("mobs.cod.attributes.max_health", oldValue); ++ } ++ codMaxHealth = getDouble("mobs.cod.attributes.max_health", codMaxHealth); ++ codTakeDamageFromWater = getBoolean("mobs.cod.takes-damage-from-water", codTakeDamageFromWater); ++ codAlwaysDropExp = getBoolean("mobs.cod.always-drop-exp", codAlwaysDropExp); ++ } ++ ++ public boolean cowRidable = false; ++ public boolean cowRidableInWater = true; ++ public boolean cowControllable = true; ++ public double cowMaxHealth = 10.0D; ++ public int cowFeedMushrooms = 0; ++ public int cowBreedingTicks = 6000; ++ public boolean cowTakeDamageFromWater = false; ++ public double cowNaturallyAggressiveToPlayersChance = 0.0D; ++ public double cowNaturallyAggressiveToPlayersDamage = 2.0D; ++ public boolean cowAlwaysDropExp = false; ++ private void cowSettings() { ++ if (PurpurConfig.version < 22) { ++ double oldValue = getDouble("mobs.cow.naturally-aggressive-to-players-chance", cowNaturallyAggressiveToPlayersChance); ++ set("mobs.cow.naturally-aggressive-to-players-chance", null); ++ set("mobs.cow.naturally-aggressive-to-players.chance", oldValue); ++ } ++ cowRidable = getBoolean("mobs.cow.ridable", cowRidable); ++ cowRidableInWater = getBoolean("mobs.cow.ridable-in-water", cowRidableInWater); ++ cowControllable = getBoolean("mobs.cow.controllable", cowControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.cow.attributes.max-health", cowMaxHealth); ++ set("mobs.cow.attributes.max-health", null); ++ set("mobs.cow.attributes.max_health", oldValue); ++ } ++ cowMaxHealth = getDouble("mobs.cow.attributes.max_health", cowMaxHealth); ++ cowFeedMushrooms = getInt("mobs.cow.feed-mushrooms-for-mooshroom", cowFeedMushrooms); ++ cowBreedingTicks = getInt("mobs.cow.breeding-delay-ticks", cowBreedingTicks); ++ cowTakeDamageFromWater = getBoolean("mobs.cow.takes-damage-from-water", cowTakeDamageFromWater); ++ cowNaturallyAggressiveToPlayersChance = getDouble("mobs.cow.naturally-aggressive-to-players.chance", cowNaturallyAggressiveToPlayersChance); ++ cowNaturallyAggressiveToPlayersDamage = getDouble("mobs.cow.naturally-aggressive-to-players.damage", cowNaturallyAggressiveToPlayersDamage); ++ cowAlwaysDropExp = getBoolean("mobs.cow.always-drop-exp", cowAlwaysDropExp); ++ } ++ ++ public boolean creeperRidable = false; ++ public boolean creeperRidableInWater = true; ++ public boolean creeperControllable = true; ++ public double creeperMaxHealth = 20.0D; ++ public double creeperChargedChance = 0.0D; ++ public boolean creeperAllowGriefing = true; ++ public boolean creeperBypassMobGriefing = false; ++ public boolean creeperTakeDamageFromWater = false; ++ public boolean creeperExplodeWhenKilled = false; ++ public boolean creeperHealthRadius = false; ++ public boolean creeperAlwaysDropExp = false; ++ public double creeperHeadVisibilityPercent = 0.5D; ++ public boolean creeperEncircleTarget = false; ++ private void creeperSettings() { ++ creeperRidable = getBoolean("mobs.creeper.ridable", creeperRidable); ++ creeperRidableInWater = getBoolean("mobs.creeper.ridable-in-water", creeperRidableInWater); ++ creeperControllable = getBoolean("mobs.creeper.controllable", creeperControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.creeper.attributes.max-health", creeperMaxHealth); ++ set("mobs.creeper.attributes.max-health", null); ++ set("mobs.creeper.attributes.max_health", oldValue); ++ } ++ creeperMaxHealth = getDouble("mobs.creeper.attributes.max_health", creeperMaxHealth); ++ creeperChargedChance = getDouble("mobs.creeper.naturally-charged-chance", creeperChargedChance); ++ creeperAllowGriefing = getBoolean("mobs.creeper.allow-griefing", creeperAllowGriefing); ++ creeperBypassMobGriefing = getBoolean("mobs.creeper.bypass-mob-griefing", creeperBypassMobGriefing); ++ creeperTakeDamageFromWater = getBoolean("mobs.creeper.takes-damage-from-water", creeperTakeDamageFromWater); ++ creeperExplodeWhenKilled = getBoolean("mobs.creeper.explode-when-killed", creeperExplodeWhenKilled); ++ creeperHealthRadius = getBoolean("mobs.creeper.health-impacts-explosion", creeperHealthRadius); ++ creeperAlwaysDropExp = getBoolean("mobs.creeper.always-drop-exp", creeperAlwaysDropExp); ++ creeperHeadVisibilityPercent = getDouble("mobs.creeper.head-visibility-percent", creeperHeadVisibilityPercent); ++ creeperEncircleTarget = getBoolean("mobs.creeper.encircle-target", creeperEncircleTarget); ++ } ++ ++ public boolean dolphinRidable = false; ++ public boolean dolphinControllable = true; ++ public int dolphinSpitCooldown = 20; ++ public float dolphinSpitSpeed = 1.0F; ++ public float dolphinSpitDamage = 2.0F; ++ public double dolphinMaxHealth = 10.0D; ++ public boolean dolphinDisableTreasureSearching = false; ++ public boolean dolphinTakeDamageFromWater = false; ++ public double dolphinNaturallyAggressiveToPlayersChance = 0.0D; ++ public boolean dolphinAlwaysDropExp = false; ++ private void dolphinSettings() { ++ dolphinRidable = getBoolean("mobs.dolphin.ridable", dolphinRidable); ++ dolphinControllable = getBoolean("mobs.dolphin.controllable", dolphinControllable); ++ dolphinSpitCooldown = getInt("mobs.dolphin.spit.cooldown", dolphinSpitCooldown); ++ dolphinSpitSpeed = (float) getDouble("mobs.dolphin.spit.speed", dolphinSpitSpeed); ++ dolphinSpitDamage = (float) getDouble("mobs.dolphin.spit.damage", dolphinSpitDamage); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.dolphin.attributes.max-health", dolphinMaxHealth); ++ set("mobs.dolphin.attributes.max-health", null); ++ set("mobs.dolphin.attributes.max_health", oldValue); ++ } ++ dolphinMaxHealth = getDouble("mobs.dolphin.attributes.max_health", dolphinMaxHealth); ++ dolphinDisableTreasureSearching = getBoolean("mobs.dolphin.disable-treasure-searching", dolphinDisableTreasureSearching); ++ dolphinTakeDamageFromWater = getBoolean("mobs.dolphin.takes-damage-from-water", dolphinTakeDamageFromWater); ++ dolphinNaturallyAggressiveToPlayersChance = getDouble("mobs.dolphin.naturally-aggressive-to-players-chance", dolphinNaturallyAggressiveToPlayersChance); ++ dolphinAlwaysDropExp = getBoolean("mobs.dolphin.always-drop-exp", dolphinAlwaysDropExp); ++ } ++ ++ public boolean donkeyRidableInWater = false; ++ public double donkeyMaxHealthMin = 15.0D; ++ public double donkeyMaxHealthMax = 30.0D; ++ public double donkeyJumpStrengthMin = 0.5D; ++ public double donkeyJumpStrengthMax = 0.5D; ++ public double donkeyMovementSpeedMin = 0.175D; ++ public double donkeyMovementSpeedMax = 0.175D; ++ public int donkeyBreedingTicks = 6000; ++ public boolean donkeyTakeDamageFromWater = false; ++ public boolean donkeyAlwaysDropExp = false; ++ private void donkeySettings() { ++ donkeyRidableInWater = getBoolean("mobs.donkey.ridable-in-water", donkeyRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.donkey.attributes.max-health.min", donkeyMaxHealthMin); ++ double oldMax = getDouble("mobs.donkey.attributes.max-health.max", donkeyMaxHealthMax); ++ set("mobs.donkey.attributes.max-health", null); ++ set("mobs.donkey.attributes.max_health.min", oldMin); ++ set("mobs.donkey.attributes.max_health.max", oldMax); ++ } ++ donkeyMaxHealthMin = getDouble("mobs.donkey.attributes.max_health.min", donkeyMaxHealthMin); ++ donkeyMaxHealthMax = getDouble("mobs.donkey.attributes.max_health.max", donkeyMaxHealthMax); ++ donkeyJumpStrengthMin = getDouble("mobs.donkey.attributes.jump_strength.min", donkeyJumpStrengthMin); ++ donkeyJumpStrengthMax = getDouble("mobs.donkey.attributes.jump_strength.max", donkeyJumpStrengthMax); ++ donkeyMovementSpeedMin = getDouble("mobs.donkey.attributes.movement_speed.min", donkeyMovementSpeedMin); ++ donkeyMovementSpeedMax = getDouble("mobs.donkey.attributes.movement_speed.max", donkeyMovementSpeedMax); ++ donkeyBreedingTicks = getInt("mobs.donkey.breeding-delay-ticks", donkeyBreedingTicks); ++ donkeyTakeDamageFromWater = getBoolean("mobs.donkey.takes-damage-from-water", donkeyTakeDamageFromWater); ++ donkeyAlwaysDropExp = getBoolean("mobs.donkey.always-drop-exp", donkeyAlwaysDropExp); ++ } ++ ++ public boolean drownedRidable = false; ++ public boolean drownedRidableInWater = true; ++ public boolean drownedControllable = true; ++ public double drownedMaxHealth = 20.0D; ++ public double drownedSpawnReinforcements = 0.1D; ++ public boolean drownedJockeyOnlyBaby = true; ++ public double drownedJockeyChance = 0.05D; ++ public boolean drownedJockeyTryExistingChickens = true; ++ public boolean drownedTakeDamageFromWater = false; ++ public boolean drownedBreakDoors = false; ++ public boolean drownedAlwaysDropExp = false; ++ private void drownedSettings() { ++ drownedRidable = getBoolean("mobs.drowned.ridable", drownedRidable); ++ drownedRidableInWater = getBoolean("mobs.drowned.ridable-in-water", drownedRidableInWater); ++ drownedControllable = getBoolean("mobs.drowned.controllable", drownedControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.drowned.attributes.max-health", drownedMaxHealth); ++ set("mobs.drowned.attributes.max-health", null); ++ set("mobs.drowned.attributes.max_health", oldValue); ++ } ++ drownedMaxHealth = getDouble("mobs.drowned.attributes.max_health", drownedMaxHealth); ++ drownedSpawnReinforcements = getDouble("mobs.drowned.attributes.spawn_reinforcements", drownedSpawnReinforcements); ++ drownedJockeyOnlyBaby = getBoolean("mobs.drowned.jockey.only-babies", drownedJockeyOnlyBaby); ++ drownedJockeyChance = getDouble("mobs.drowned.jockey.chance", drownedJockeyChance); ++ drownedJockeyTryExistingChickens = getBoolean("mobs.drowned.jockey.try-existing-chickens", drownedJockeyTryExistingChickens); ++ drownedTakeDamageFromWater = getBoolean("mobs.drowned.takes-damage-from-water", drownedTakeDamageFromWater); ++ drownedBreakDoors = getBoolean("mobs.drowned.can-break-doors", drownedBreakDoors); ++ drownedAlwaysDropExp = getBoolean("mobs.drowned.always-drop-exp", drownedAlwaysDropExp); ++ } ++ ++ public boolean elderGuardianRidable = false; ++ public boolean elderGuardianControllable = true; ++ public double elderGuardianMaxHealth = 80.0D; ++ public boolean elderGuardianTakeDamageFromWater = false; ++ public boolean elderGuardianAlwaysDropExp = false; ++ private void elderGuardianSettings() { ++ elderGuardianRidable = getBoolean("mobs.elder_guardian.ridable", elderGuardianRidable); ++ elderGuardianControllable = getBoolean("mobs.elder_guardian.controllable", elderGuardianControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.elder_guardian.attributes.max-health", elderGuardianMaxHealth); ++ set("mobs.elder_guardian.attributes.max-health", null); ++ set("mobs.elder_guardian.attributes.max_health", oldValue); ++ } ++ elderGuardianMaxHealth = getDouble("mobs.elder_guardian.attributes.max_health", elderGuardianMaxHealth); ++ elderGuardianTakeDamageFromWater = getBoolean("mobs.elder_guardian.takes-damage-from-water", elderGuardianTakeDamageFromWater); ++ elderGuardianAlwaysDropExp = getBoolean("mobs.elder_guardian.always-drop-exp", elderGuardianAlwaysDropExp); ++ } ++ ++ public boolean enchantmentTableLapisPersists = false; ++ private void enchantmentTableSettings() { ++ enchantmentTableLapisPersists = getBoolean("blocks.enchantment-table.lapis-persists", enchantmentTableLapisPersists); ++ } ++ ++ public boolean enderDragonRidable = false; ++ public boolean enderDragonRidableInWater = true; ++ public boolean enderDragonControllable = true; ++ public double enderDragonMaxY = 320D; ++ public double enderDragonMaxHealth = 200.0D; ++ public boolean enderDragonAlwaysDropsFullExp = false; ++ public boolean enderDragonBypassMobGriefing = false; ++ public boolean enderDragonTakeDamageFromWater = false; ++ public boolean enderDragonCanRideVehicles = false; ++ private void enderDragonSettings() { ++ enderDragonRidable = getBoolean("mobs.ender_dragon.ridable", enderDragonRidable); ++ enderDragonRidableInWater = getBoolean("mobs.ender_dragon.ridable-in-water", enderDragonRidableInWater); ++ enderDragonControllable = getBoolean("mobs.ender_dragon.controllable", enderDragonControllable); ++ enderDragonMaxY = getDouble("mobs.ender_dragon.ridable-max-y", enderDragonMaxY); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.ender_dragon.max-health", enderDragonMaxHealth); ++ set("mobs.ender_dragon.max-health", null); ++ set("mobs.ender_dragon.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ender_dragon.attributes.max-health", enderDragonMaxHealth); ++ set("mobs.ender_dragon.attributes.max-health", null); ++ set("mobs.ender_dragon.attributes.max_health", oldValue); ++ } ++ enderDragonMaxHealth = getDouble("mobs.ender_dragon.attributes.max_health", enderDragonMaxHealth); ++ enderDragonAlwaysDropsFullExp = getBoolean("mobs.ender_dragon.always-drop-full-exp", enderDragonAlwaysDropsFullExp); ++ enderDragonBypassMobGriefing = getBoolean("mobs.ender_dragon.bypass-mob-griefing", enderDragonBypassMobGriefing); ++ enderDragonTakeDamageFromWater = getBoolean("mobs.ender_dragon.takes-damage-from-water", enderDragonTakeDamageFromWater); ++ enderDragonCanRideVehicles = getBoolean("mobs.ender_dragon.can-ride-vehicles", enderDragonCanRideVehicles); ++ } ++ ++ public boolean endermanRidable = false; ++ public boolean endermanRidableInWater = true; ++ public boolean endermanControllable = true; ++ public double endermanMaxHealth = 40.0D; ++ public boolean endermanAllowGriefing = true; ++ public boolean endermanDespawnEvenWithBlock = false; ++ public boolean endermanBypassMobGriefing = false; ++ public boolean endermanTakeDamageFromWater = true; ++ public boolean endermanAggroEndermites = true; ++ public boolean endermanAggroEndermitesOnlyIfPlayerSpawned = false; ++ public boolean endermanIgnorePlayerDragonHead = false; ++ public boolean endermanDisableStareAggro = false; ++ public boolean endermanIgnoreProjectiles = false; ++ public boolean endermanAlwaysDropExp = false; ++ private void endermanSettings() { ++ endermanRidable = getBoolean("mobs.enderman.ridable", endermanRidable); ++ endermanRidableInWater = getBoolean("mobs.enderman.ridable-in-water", endermanRidableInWater); ++ endermanControllable = getBoolean("mobs.enderman.controllable", endermanControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.enderman.attributes.max-health", endermanMaxHealth); ++ set("mobs.enderman.attributes.max-health", null); ++ set("mobs.enderman.attributes.max_health", oldValue); ++ } ++ if (PurpurConfig.version < 15) { ++ // remove old option ++ set("mobs.enderman.aggressive-towards-spawned-endermites", null); ++ } ++ endermanMaxHealth = getDouble("mobs.enderman.attributes.max_health", endermanMaxHealth); ++ endermanAllowGriefing = getBoolean("mobs.enderman.allow-griefing", endermanAllowGriefing); ++ endermanDespawnEvenWithBlock = getBoolean("mobs.enderman.can-despawn-with-held-block", endermanDespawnEvenWithBlock); ++ endermanBypassMobGriefing = getBoolean("mobs.enderman.bypass-mob-griefing", endermanBypassMobGriefing); ++ endermanTakeDamageFromWater = getBoolean("mobs.enderman.takes-damage-from-water", endermanTakeDamageFromWater); ++ endermanAggroEndermites = getBoolean("mobs.enderman.aggressive-towards-endermites", endermanAggroEndermites); ++ endermanAggroEndermitesOnlyIfPlayerSpawned = getBoolean("mobs.enderman.aggressive-towards-endermites-only-spawned-by-player-thrown-ender-pearls", endermanAggroEndermitesOnlyIfPlayerSpawned); ++ endermanIgnorePlayerDragonHead = getBoolean("mobs.enderman.ignore-players-wearing-dragon-head", endermanIgnorePlayerDragonHead); ++ endermanDisableStareAggro = getBoolean("mobs.enderman.disable-player-stare-aggression", endermanDisableStareAggro); ++ endermanIgnoreProjectiles = getBoolean("mobs.enderman.ignore-projectiles", endermanIgnoreProjectiles); ++ endermanAlwaysDropExp = getBoolean("mobs.enderman.always-drop-exp", endermanAlwaysDropExp); ++ } ++ ++ public boolean endermiteRidable = false; ++ public boolean endermiteRidableInWater = true; ++ public boolean endermiteControllable = true; ++ public double endermiteMaxHealth = 8.0D; ++ public boolean endermiteTakeDamageFromWater = false; ++ public boolean endermiteAlwaysDropExp = false; ++ private void endermiteSettings() { ++ endermiteRidable = getBoolean("mobs.endermite.ridable", endermiteRidable); ++ endermiteRidableInWater = getBoolean("mobs.endermite.ridable-in-water", endermiteRidableInWater); ++ endermiteControllable = getBoolean("mobs.endermite.controllable", endermiteControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.endermite.attributes.max-health", endermiteMaxHealth); ++ set("mobs.endermite.attributes.max-health", null); ++ set("mobs.endermite.attributes.max_health", oldValue); ++ } ++ endermiteMaxHealth = getDouble("mobs.endermite.attributes.max_health", endermiteMaxHealth); ++ endermiteTakeDamageFromWater = getBoolean("mobs.endermite.takes-damage-from-water", endermiteTakeDamageFromWater); ++ endermiteAlwaysDropExp = getBoolean("mobs.endermite.always-drop-exp", endermiteAlwaysDropExp); ++ } ++ ++ public boolean evokerRidable = false; ++ public boolean evokerRidableInWater = true; ++ public boolean evokerControllable = true; ++ public double evokerMaxHealth = 24.0D; ++ public boolean evokerBypassMobGriefing = false; ++ public boolean evokerTakeDamageFromWater = false; ++ public boolean evokerAlwaysDropExp = false; ++ private void evokerSettings() { ++ evokerRidable = getBoolean("mobs.evoker.ridable", evokerRidable); ++ evokerRidableInWater = getBoolean("mobs.evoker.ridable-in-water", evokerRidableInWater); ++ evokerControllable = getBoolean("mobs.evoker.controllable", evokerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.evoker.attributes.max-health", evokerMaxHealth); ++ set("mobs.evoker.attributes.max-health", null); ++ set("mobs.evoker.attributes.max_health", oldValue); ++ } ++ evokerMaxHealth = getDouble("mobs.evoker.attributes.max_health", evokerMaxHealth); ++ evokerBypassMobGriefing = getBoolean("mobs.evoker.bypass-mob-griefing", evokerBypassMobGriefing); ++ evokerTakeDamageFromWater = getBoolean("mobs.evoker.takes-damage-from-water", evokerTakeDamageFromWater); ++ evokerAlwaysDropExp = getBoolean("mobs.evoker.always-drop-exp", evokerAlwaysDropExp); ++ } ++ ++ public boolean foxRidable = false; ++ public boolean foxRidableInWater = true; ++ public boolean foxControllable = true; ++ public double foxMaxHealth = 10.0D; ++ public boolean foxTypeChangesWithTulips = false; ++ public int foxBreedingTicks = 6000; ++ public boolean foxBypassMobGriefing = false; ++ public boolean foxTakeDamageFromWater = false; ++ public boolean foxAlwaysDropExp = false; ++ private void foxSettings() { ++ foxRidable = getBoolean("mobs.fox.ridable", foxRidable); ++ foxRidableInWater = getBoolean("mobs.fox.ridable-in-water", foxRidableInWater); ++ foxControllable = getBoolean("mobs.fox.controllable", foxControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.fox.attributes.max-health", foxMaxHealth); ++ set("mobs.fox.attributes.max-health", null); ++ set("mobs.fox.attributes.max_health", oldValue); ++ } ++ foxMaxHealth = getDouble("mobs.fox.attributes.max_health", foxMaxHealth); ++ foxTypeChangesWithTulips = getBoolean("mobs.fox.tulips-change-type", foxTypeChangesWithTulips); ++ foxBreedingTicks = getInt("mobs.fox.breeding-delay-ticks", foxBreedingTicks); ++ foxBypassMobGriefing = getBoolean("mobs.fox.bypass-mob-griefing", foxBypassMobGriefing); ++ foxTakeDamageFromWater = getBoolean("mobs.fox.takes-damage-from-water", foxTakeDamageFromWater); ++ foxAlwaysDropExp = getBoolean("mobs.fox.always-drop-exp", foxAlwaysDropExp); ++ } ++ ++ public boolean frogRidable = false; ++ public boolean frogRidableInWater = true; ++ public boolean frogControllable = true; ++ public float frogRidableJumpHeight = 0.65F; ++ public int frogBreedingTicks = 6000; ++ private void frogSettings() { ++ frogRidable = getBoolean("mobs.frog.ridable", frogRidable); ++ frogRidableInWater = getBoolean("mobs.frog.ridable-in-water", frogRidableInWater); ++ frogControllable = getBoolean("mobs.frog.controllable", frogControllable); ++ frogRidableJumpHeight = (float) getDouble("mobs.frog.ridable-jump-height", frogRidableJumpHeight); ++ frogBreedingTicks = getInt("mobs.frog.breeding-delay-ticks", frogBreedingTicks); ++ } ++ ++ public boolean ghastRidable = false; ++ public boolean ghastRidableInWater = true; ++ public boolean ghastControllable = true; ++ public double ghastMaxY = 320D; ++ public double ghastMaxHealth = 10.0D; ++ public boolean ghastTakeDamageFromWater = false; ++ public boolean ghastAlwaysDropExp = false; ++ private void ghastSettings() { ++ ghastRidable = getBoolean("mobs.ghast.ridable", ghastRidable); ++ ghastRidableInWater = getBoolean("mobs.ghast.ridable-in-water", ghastRidableInWater); ++ ghastControllable = getBoolean("mobs.ghast.controllable", ghastControllable); ++ ghastMaxY = getDouble("mobs.ghast.ridable-max-y", ghastMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ghast.attributes.max-health", ghastMaxHealth); ++ set("mobs.ghast.attributes.max-health", null); ++ set("mobs.ghast.attributes.max_health", oldValue); ++ } ++ ghastMaxHealth = getDouble("mobs.ghast.attributes.max_health", ghastMaxHealth); ++ ghastTakeDamageFromWater = getBoolean("mobs.ghast.takes-damage-from-water", ghastTakeDamageFromWater); ++ ghastAlwaysDropExp = getBoolean("mobs.ghast.always-drop-exp", ghastAlwaysDropExp); ++ } ++ ++ public boolean giantRidable = false; ++ public boolean giantRidableInWater = true; ++ public boolean giantControllable = true; ++ public double giantMovementSpeed = 0.5D; ++ public double giantAttackDamage = 50.0D; ++ public double giantMaxHealth = 100.0D; ++ public float giantStepHeight = 2.0F; ++ public float giantJumpHeight = 1.0F; ++ public boolean giantHaveAI = false; ++ public boolean giantHaveHostileAI = false; ++ public boolean giantTakeDamageFromWater = false; ++ public boolean giantAlwaysDropExp = false; ++ private void giantSettings() { ++ giantRidable = getBoolean("mobs.giant.ridable", giantRidable); ++ giantRidableInWater = getBoolean("mobs.giant.ridable-in-water", giantRidableInWater); ++ giantControllable = getBoolean("mobs.giant.controllable", giantControllable); ++ giantMovementSpeed = getDouble("mobs.giant.movement-speed", giantMovementSpeed); ++ giantAttackDamage = getDouble("mobs.giant.attack-damage", giantAttackDamage); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.giant.max-health", giantMaxHealth); ++ set("mobs.giant.max-health", null); ++ set("mobs.giant.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.giant.attributes.max-health", giantMaxHealth); ++ set("mobs.giant.attributes.max-health", null); ++ set("mobs.giant.attributes.max_health", oldValue); ++ } ++ giantMaxHealth = getDouble("mobs.giant.attributes.max_health", giantMaxHealth); ++ giantStepHeight = (float) getDouble("mobs.giant.step-height", giantStepHeight); ++ giantJumpHeight = (float) getDouble("mobs.giant.jump-height", giantJumpHeight); ++ giantHaveAI = getBoolean("mobs.giant.have-ai", giantHaveAI); ++ giantHaveHostileAI = getBoolean("mobs.giant.have-hostile-ai", giantHaveHostileAI); ++ giantTakeDamageFromWater = getBoolean("mobs.giant.takes-damage-from-water", giantTakeDamageFromWater); ++ giantAlwaysDropExp = getBoolean("mobs.giant.always-drop-exp", giantAlwaysDropExp); ++ } ++ ++ public boolean glowSquidRidable = false; ++ public boolean glowSquidControllable = true; ++ public double glowSquidMaxHealth = 10.0D; ++ public boolean glowSquidsCanFly = false; ++ public boolean glowSquidTakeDamageFromWater = false; ++ public boolean glowSquidAlwaysDropExp = false; ++ public GlowSquidColor.Mode glowSquidColorMode = GlowSquidColor.Mode.RAINBOW; ++ private void glowSquidSettings() { ++ glowSquidRidable = getBoolean("mobs.glow_squid.ridable", glowSquidRidable); ++ glowSquidControllable = getBoolean("mobs.glow_squid.controllable", glowSquidControllable); ++ glowSquidMaxHealth = getDouble("mobs.glow_squid.attributes.max_health", glowSquidMaxHealth); ++ glowSquidsCanFly = getBoolean("mobs.glow_squid.can-fly", glowSquidsCanFly); ++ glowSquidTakeDamageFromWater = getBoolean("mobs.glow_squid.takes-damage-from-water", glowSquidTakeDamageFromWater); ++ glowSquidAlwaysDropExp = getBoolean("mobs.glow_squid.always-drop-exp", glowSquidAlwaysDropExp); ++ glowSquidColorMode = GlowSquidColor.Mode.get(getString("mobs.glow_squid.rainglow-mode", glowSquidColorMode.toString())); ++ } ++ ++ public boolean goatRidable = false; ++ public boolean goatRidableInWater = true; ++ public boolean goatControllable = true; ++ public double goatMaxHealth = 10.0D; ++ public int goatBreedingTicks = 6000; ++ public boolean goatTakeDamageFromWater = false; ++ public boolean goatAlwaysDropExp = false; ++ private void goatSettings() { ++ goatRidable = getBoolean("mobs.goat.ridable", goatRidable); ++ goatRidableInWater = getBoolean("mobs.goat.ridable-in-water", goatRidableInWater); ++ goatControllable = getBoolean("mobs.goat.controllable", goatControllable); ++ goatMaxHealth = getDouble("mobs.goat.attributes.max_health", goatMaxHealth); ++ goatBreedingTicks = getInt("mobs.goat.breeding-delay-ticks", goatBreedingTicks); ++ goatTakeDamageFromWater = getBoolean("mobs.goat.takes-damage-from-water", goatTakeDamageFromWater); ++ goatAlwaysDropExp = getBoolean("mobs.goat.always-drop-exp", goatAlwaysDropExp); ++ } ++ ++ public boolean guardianRidable = false; ++ public boolean guardianControllable = true; ++ public double guardianMaxHealth = 30.0D; ++ public boolean guardianTakeDamageFromWater = false; ++ public boolean guardianAlwaysDropExp = false; ++ private void guardianSettings() { ++ guardianRidable = getBoolean("mobs.guardian.ridable", guardianRidable); ++ guardianControllable = getBoolean("mobs.guardian.controllable", guardianControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.guardian.attributes.max-health", guardianMaxHealth); ++ set("mobs.guardian.attributes.max-health", null); ++ set("mobs.guardian.attributes.max_health", oldValue); ++ } ++ guardianMaxHealth = getDouble("mobs.guardian.attributes.max_health", guardianMaxHealth); ++ guardianTakeDamageFromWater = getBoolean("mobs.guardian.takes-damage-from-water", guardianTakeDamageFromWater); ++ guardianAlwaysDropExp = getBoolean("mobs.guardian.always-drop-exp", guardianAlwaysDropExp); ++ } ++ ++ public boolean forceHalloweenSeason = false; ++ public float chanceHeadHalloweenOnEntity = 0.25F; ++ private void halloweenSetting() { ++ forceHalloweenSeason = getBoolean("gameplay-mechanics.halloween.force", forceHalloweenSeason); ++ chanceHeadHalloweenOnEntity = (float) getDouble("gameplay-mechanics.halloween.head-chance", chanceHeadHalloweenOnEntity); ++ } ++ ++ public boolean hoglinRidable = false; ++ public boolean hoglinRidableInWater = true; ++ public boolean hoglinControllable = true; ++ public double hoglinMaxHealth = 40.0D; ++ public int hoglinBreedingTicks = 6000; ++ public boolean hoglinTakeDamageFromWater = false; ++ public boolean hoglinAlwaysDropExp = false; ++ private void hoglinSettings() { ++ hoglinRidable = getBoolean("mobs.hoglin.ridable", hoglinRidable); ++ hoglinRidableInWater = getBoolean("mobs.hoglin.ridable-in-water", hoglinRidableInWater); ++ hoglinControllable = getBoolean("mobs.hoglin.controllable", hoglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.hoglin.attributes.max-health", hoglinMaxHealth); ++ set("mobs.hoglin.attributes.max-health", null); ++ set("mobs.hoglin.attributes.max_health", oldValue); ++ } ++ hoglinMaxHealth = getDouble("mobs.hoglin.attributes.max_health", hoglinMaxHealth); ++ hoglinBreedingTicks = getInt("mobs.hoglin.breeding-delay-ticks", hoglinBreedingTicks); ++ hoglinTakeDamageFromWater = getBoolean("mobs.hoglin.takes-damage-from-water", hoglinTakeDamageFromWater); ++ hoglinAlwaysDropExp = getBoolean("mobs.hoglin.always-drop-exp", hoglinAlwaysDropExp); ++ } ++ ++ public boolean horseRidableInWater = false; ++ public double horseMaxHealthMin = 15.0D; ++ public double horseMaxHealthMax = 30.0D; ++ public double horseJumpStrengthMin = 0.4D; ++ public double horseJumpStrengthMax = 1.0D; ++ public double horseMovementSpeedMin = 0.1125D; ++ public double horseMovementSpeedMax = 0.3375D; ++ public int horseBreedingTicks = 6000; ++ public boolean horseTakeDamageFromWater = false; ++ public boolean horseAlwaysDropExp = false; ++ private void horseSettings() { ++ horseRidableInWater = getBoolean("mobs.horse.ridable-in-water", horseRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.horse.attributes.max-health.min", horseMaxHealthMin); ++ double oldMax = getDouble("mobs.horse.attributes.max-health.max", horseMaxHealthMax); ++ set("mobs.horse.attributes.max-health", null); ++ set("mobs.horse.attributes.max_health.min", oldMin); ++ set("mobs.horse.attributes.max_health.max", oldMax); ++ } ++ horseMaxHealthMin = getDouble("mobs.horse.attributes.max_health.min", horseMaxHealthMin); ++ horseMaxHealthMax = getDouble("mobs.horse.attributes.max_health.max", horseMaxHealthMax); ++ horseJumpStrengthMin = getDouble("mobs.horse.attributes.jump_strength.min", horseJumpStrengthMin); ++ horseJumpStrengthMax = getDouble("mobs.horse.attributes.jump_strength.max", horseJumpStrengthMax); ++ horseMovementSpeedMin = getDouble("mobs.horse.attributes.movement_speed.min", horseMovementSpeedMin); ++ horseMovementSpeedMax = getDouble("mobs.horse.attributes.movement_speed.max", horseMovementSpeedMax); ++ horseBreedingTicks = getInt("mobs.horse.breeding-delay-ticks", horseBreedingTicks); ++ horseTakeDamageFromWater = getBoolean("mobs.horse.takes-damage-from-water", horseTakeDamageFromWater); ++ horseAlwaysDropExp = getBoolean("mobs.horse.always-drop-exp", horseAlwaysDropExp); ++ } ++ ++ public boolean huskRidable = false; ++ public boolean huskRidableInWater = true; ++ public boolean huskControllable = true; ++ public double huskMaxHealth = 20.0D; ++ public double huskSpawnReinforcements = 0.1D; ++ public boolean huskJockeyOnlyBaby = true; ++ public double huskJockeyChance = 0.05D; ++ public boolean huskJockeyTryExistingChickens = true; ++ public boolean huskTakeDamageFromWater = false; ++ public boolean huskAlwaysDropExp = false; ++ private void huskSettings() { ++ huskRidable = getBoolean("mobs.husk.ridable", huskRidable); ++ huskRidableInWater = getBoolean("mobs.husk.ridable-in-water", huskRidableInWater); ++ huskControllable = getBoolean("mobs.husk.controllable", huskControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.husk.attributes.max-health", huskMaxHealth); ++ set("mobs.husk.attributes.max-health", null); ++ set("mobs.husk.attributes.max_health", oldValue); ++ } ++ huskMaxHealth = getDouble("mobs.husk.attributes.max_health", huskMaxHealth); ++ huskSpawnReinforcements = getDouble("mobs.husk.attributes.spawn_reinforcements", huskSpawnReinforcements); ++ huskJockeyOnlyBaby = getBoolean("mobs.husk.jockey.only-babies", huskJockeyOnlyBaby); ++ huskJockeyChance = getDouble("mobs.husk.jockey.chance", huskJockeyChance); ++ huskJockeyTryExistingChickens = getBoolean("mobs.husk.jockey.try-existing-chickens", huskJockeyTryExistingChickens); ++ huskTakeDamageFromWater = getBoolean("mobs.husk.takes-damage-from-water", huskTakeDamageFromWater); ++ huskAlwaysDropExp = getBoolean("mobs.husk.always-drop-exp", huskAlwaysDropExp); ++ } ++ ++ public boolean illusionerRidable = false; ++ public boolean illusionerRidableInWater = true; ++ public boolean illusionerControllable = true; ++ public double illusionerMovementSpeed = 0.5D; ++ public double illusionerFollowRange = 18.0D; ++ public double illusionerMaxHealth = 32.0D; ++ public boolean illusionerTakeDamageFromWater = false; ++ public boolean illusionerAlwaysDropExp = false; ++ private void illusionerSettings() { ++ illusionerRidable = getBoolean("mobs.illusioner.ridable", illusionerRidable); ++ illusionerRidableInWater = getBoolean("mobs.illusioner.ridable-in-water", illusionerRidableInWater); ++ illusionerControllable = getBoolean("mobs.illusioner.controllable", illusionerControllable); ++ illusionerMovementSpeed = getDouble("mobs.illusioner.movement-speed", illusionerMovementSpeed); ++ illusionerFollowRange = getDouble("mobs.illusioner.follow-range", illusionerFollowRange); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.illusioner.max-health", illusionerMaxHealth); ++ set("mobs.illusioner.max-health", null); ++ set("mobs.illusioner.attributes.max_health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.illusioner.attributes.max-health", illusionerMaxHealth); ++ set("mobs.illusioner.attributes.max-health", null); ++ set("mobs.illusioner.attributes.max_health", oldValue); ++ } ++ illusionerMaxHealth = getDouble("mobs.illusioner.attributes.max_health", illusionerMaxHealth); ++ illusionerTakeDamageFromWater = getBoolean("mobs.illusioner.takes-damage-from-water", illusionerTakeDamageFromWater); ++ illusionerAlwaysDropExp = getBoolean("mobs.illusioner.always-drop-exp", illusionerAlwaysDropExp); ++ } ++ ++ public boolean ironGolemRidable = false; ++ public boolean ironGolemRidableInWater = true; ++ public boolean ironGolemControllable = true; ++ public boolean ironGolemCanSwim = false; ++ public double ironGolemMaxHealth = 100.0D; ++ public boolean ironGolemTakeDamageFromWater = false; ++ public boolean ironGolemPoppyCalm = false; ++ public boolean ironGolemHealCalm = false; ++ public boolean ironGolemAlwaysDropExp = false; ++ private void ironGolemSettings() { ++ ironGolemRidable = getBoolean("mobs.iron_golem.ridable", ironGolemRidable); ++ ironGolemRidableInWater = getBoolean("mobs.iron_golem.ridable-in-water", ironGolemRidableInWater); ++ ironGolemControllable = getBoolean("mobs.iron_golem.controllable", ironGolemControllable); ++ ironGolemCanSwim = getBoolean("mobs.iron_golem.can-swim", ironGolemCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.iron_golem.attributes.max-health", ironGolemMaxHealth); ++ set("mobs.iron_golem.attributes.max-health", null); ++ set("mobs.iron_golem.attributes.max_health", oldValue); ++ } ++ ironGolemMaxHealth = getDouble("mobs.iron_golem.attributes.max_health", ironGolemMaxHealth); ++ ironGolemTakeDamageFromWater = getBoolean("mobs.iron_golem.takes-damage-from-water", ironGolemTakeDamageFromWater); ++ ironGolemPoppyCalm = getBoolean("mobs.iron_golem.poppy-calms-anger", ironGolemPoppyCalm); ++ ironGolemHealCalm = getBoolean("mobs.iron_golem.healing-calms-anger", ironGolemHealCalm); ++ ironGolemAlwaysDropExp = getBoolean("mobs.iron_golem.always-drop-exp", ironGolemAlwaysDropExp); ++ } ++ ++ public boolean llamaRidable = false; ++ public boolean llamaRidableInWater = false; ++ public boolean llamaControllable = true; ++ public double llamaMaxHealthMin = 15.0D; ++ public double llamaMaxHealthMax = 30.0D; ++ public double llamaJumpStrengthMin = 0.5D; ++ public double llamaJumpStrengthMax = 0.5D; ++ public double llamaMovementSpeedMin = 0.175D; ++ public double llamaMovementSpeedMax = 0.175D; ++ public int llamaBreedingTicks = 6000; ++ public boolean llamaTakeDamageFromWater = false; ++ public boolean llamaJoinCaravans = true; ++ public boolean llamaAlwaysDropExp = false; ++ private void llamaSettings() { ++ llamaRidable = getBoolean("mobs.llama.ridable", llamaRidable); ++ llamaRidableInWater = getBoolean("mobs.llama.ridable-in-water", llamaRidableInWater); ++ llamaControllable = getBoolean("mobs.llama.controllable", llamaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.llama.attributes.max-health.min", llamaMaxHealthMin); ++ double oldMax = getDouble("mobs.llama.attributes.max-health.max", llamaMaxHealthMax); ++ set("mobs.llama.attributes.max-health", null); ++ set("mobs.llama.attributes.max_health.min", oldMin); ++ set("mobs.llama.attributes.max_health.max", oldMax); ++ } ++ llamaMaxHealthMin = getDouble("mobs.llama.attributes.max_health.min", llamaMaxHealthMin); ++ llamaMaxHealthMax = getDouble("mobs.llama.attributes.max_health.max", llamaMaxHealthMax); ++ llamaJumpStrengthMin = getDouble("mobs.llama.attributes.jump_strength.min", llamaJumpStrengthMin); ++ llamaJumpStrengthMax = getDouble("mobs.llama.attributes.jump_strength.max", llamaJumpStrengthMax); ++ llamaMovementSpeedMin = getDouble("mobs.llama.attributes.movement_speed.min", llamaMovementSpeedMin); ++ llamaMovementSpeedMax = getDouble("mobs.llama.attributes.movement_speed.max", llamaMovementSpeedMax); ++ llamaBreedingTicks = getInt("mobs.llama.breeding-delay-ticks", llamaBreedingTicks); ++ llamaTakeDamageFromWater = getBoolean("mobs.llama.takes-damage-from-water", llamaTakeDamageFromWater); ++ llamaJoinCaravans = getBoolean("mobs.llama.join-caravans", llamaJoinCaravans); ++ llamaAlwaysDropExp = getBoolean("mobs.llama.always-drop-exp", llamaAlwaysDropExp); ++ } ++ ++ public boolean magmaCubeRidable = false; ++ public boolean magmaCubeRidableInWater = true; ++ public boolean magmaCubeControllable = true; ++ public String magmaCubeMaxHealth = "size * size"; ++ public String magmaCubeAttackDamage = "size"; ++ public Map magmaCubeMaxHealthCache = new HashMap<>(); ++ public Map magmaCubeAttackDamageCache = new HashMap<>(); ++ public boolean magmaCubeTakeDamageFromWater = false; ++ public boolean magmaCubeAlwaysDropExp = false; ++ private void magmaCubeSettings() { ++ magmaCubeRidable = getBoolean("mobs.magma_cube.ridable", magmaCubeRidable); ++ magmaCubeRidableInWater = getBoolean("mobs.magma_cube.ridable-in-water", magmaCubeRidableInWater); ++ magmaCubeControllable = getBoolean("mobs.magma_cube.controllable", magmaCubeControllable); ++ if (PurpurConfig.version < 10) { ++ String oldValue = getString("mobs.magma_cube.attributes.max-health", magmaCubeMaxHealth); ++ set("mobs.magma_cube.attributes.max-health", null); ++ set("mobs.magma_cube.attributes.max_health", oldValue); ++ } ++ magmaCubeMaxHealth = getString("mobs.magma_cube.attributes.max_health", magmaCubeMaxHealth); ++ magmaCubeAttackDamage = getString("mobs.magma_cube.attributes.attack_damage", magmaCubeAttackDamage); ++ magmaCubeMaxHealthCache.clear(); ++ magmaCubeAttackDamageCache.clear(); ++ magmaCubeTakeDamageFromWater = getBoolean("mobs.magma_cube.takes-damage-from-water", magmaCubeTakeDamageFromWater); ++ magmaCubeAlwaysDropExp = getBoolean("mobs.magma_cube.always-drop-exp", magmaCubeAlwaysDropExp); ++ } ++ ++ public boolean mooshroomRidable = false; ++ public boolean mooshroomRidableInWater = true; ++ public boolean mooshroomControllable = true; ++ public double mooshroomMaxHealth = 10.0D; ++ public int mooshroomBreedingTicks = 6000; ++ public boolean mooshroomTakeDamageFromWater = false; ++ public boolean mooshroomAlwaysDropExp = false; ++ private void mooshroomSettings() { ++ mooshroomRidable = getBoolean("mobs.mooshroom.ridable", mooshroomRidable); ++ mooshroomRidableInWater = getBoolean("mobs.mooshroom.ridable-in-water", mooshroomRidableInWater); ++ mooshroomControllable = getBoolean("mobs.mooshroom.controllable", mooshroomControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.mooshroom.attributes.max-health", mooshroomMaxHealth); ++ set("mobs.mooshroom.attributes.max-health", null); ++ set("mobs.mooshroom.attributes.max_health", oldValue); ++ } ++ mooshroomMaxHealth = getDouble("mobs.mooshroom.attributes.max_health", mooshroomMaxHealth); ++ mooshroomBreedingTicks = getInt("mobs.mooshroom.breeding-delay-ticks", mooshroomBreedingTicks); ++ mooshroomTakeDamageFromWater = getBoolean("mobs.mooshroom.takes-damage-from-water", mooshroomTakeDamageFromWater); ++ mooshroomAlwaysDropExp = getBoolean("mobs.mooshroom.always-drop-exp", mooshroomAlwaysDropExp); ++ } ++ ++ public boolean muleRidableInWater = false; ++ public double muleMaxHealthMin = 15.0D; ++ public double muleMaxHealthMax = 30.0D; ++ public double muleJumpStrengthMin = 0.5D; ++ public double muleJumpStrengthMax = 0.5D; ++ public double muleMovementSpeedMin = 0.175D; ++ public double muleMovementSpeedMax = 0.175D; ++ public int muleBreedingTicks = 6000; ++ public boolean muleTakeDamageFromWater = false; ++ public boolean muleAlwaysDropExp = false; ++ private void muleSettings() { ++ muleRidableInWater = getBoolean("mobs.mule.ridable-in-water", muleRidableInWater); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.mule.attributes.max-health.min", muleMaxHealthMin); ++ double oldMax = getDouble("mobs.mule.attributes.max-health.max", muleMaxHealthMax); ++ set("mobs.mule.attributes.max-health", null); ++ set("mobs.mule.attributes.max_health.min", oldMin); ++ set("mobs.mule.attributes.max_health.max", oldMax); ++ } ++ muleMaxHealthMin = getDouble("mobs.mule.attributes.max_health.min", muleMaxHealthMin); ++ muleMaxHealthMax = getDouble("mobs.mule.attributes.max_health.max", muleMaxHealthMax); ++ muleJumpStrengthMin = getDouble("mobs.mule.attributes.jump_strength.min", muleJumpStrengthMin); ++ muleJumpStrengthMax = getDouble("mobs.mule.attributes.jump_strength.max", muleJumpStrengthMax); ++ muleMovementSpeedMin = getDouble("mobs.mule.attributes.movement_speed.min", muleMovementSpeedMin); ++ muleMovementSpeedMax = getDouble("mobs.mule.attributes.movement_speed.max", muleMovementSpeedMax); ++ muleBreedingTicks = getInt("mobs.mule.breeding-delay-ticks", muleBreedingTicks); ++ muleTakeDamageFromWater = getBoolean("mobs.mule.takes-damage-from-water", muleTakeDamageFromWater); ++ muleAlwaysDropExp = getBoolean("mobs.mule.always-drop-exp", muleAlwaysDropExp); ++ } ++ ++ public boolean ocelotRidable = false; ++ public boolean ocelotRidableInWater = true; ++ public boolean ocelotControllable = true; ++ public double ocelotMaxHealth = 10.0D; ++ public int ocelotBreedingTicks = 6000; ++ public boolean ocelotTakeDamageFromWater = false; ++ public boolean ocelotAlwaysDropExp = false; ++ private void ocelotSettings() { ++ ocelotRidable = getBoolean("mobs.ocelot.ridable", ocelotRidable); ++ ocelotRidableInWater = getBoolean("mobs.ocelot.ridable-in-water", ocelotRidableInWater); ++ ocelotControllable = getBoolean("mobs.ocelot.controllable", ocelotControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ocelot.attributes.max-health", ocelotMaxHealth); ++ set("mobs.ocelot.attributes.max-health", null); ++ set("mobs.ocelot.attributes.max_health", oldValue); ++ } ++ ocelotMaxHealth = getDouble("mobs.ocelot.attributes.max_health", ocelotMaxHealth); ++ ocelotBreedingTicks = getInt("mobs.ocelot.breeding-delay-ticks", ocelotBreedingTicks); ++ ocelotTakeDamageFromWater = getBoolean("mobs.ocelot.takes-damage-from-water", ocelotTakeDamageFromWater); ++ ocelotAlwaysDropExp = getBoolean("mobs.ocelot.always-drop-exp", ocelotAlwaysDropExp); ++ } ++ ++ public boolean pandaRidable = false; ++ public boolean pandaRidableInWater = true; ++ public boolean pandaControllable = true; ++ public double pandaMaxHealth = 20.0D; ++ public int pandaBreedingTicks = 6000; ++ public boolean pandaTakeDamageFromWater = false; ++ public boolean pandaAlwaysDropExp = false; ++ private void pandaSettings() { ++ pandaRidable = getBoolean("mobs.panda.ridable", pandaRidable); ++ pandaRidableInWater = getBoolean("mobs.panda.ridable-in-water", pandaRidableInWater); ++ pandaControllable = getBoolean("mobs.panda.controllable", pandaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.panda.attributes.max-health", pandaMaxHealth); ++ set("mobs.panda.attributes.max-health", null); ++ set("mobs.panda.attributes.max_health", oldValue); ++ } ++ pandaMaxHealth = getDouble("mobs.panda.attributes.max_health", pandaMaxHealth); ++ pandaBreedingTicks = getInt("mobs.panda.breeding-delay-ticks", pandaBreedingTicks); ++ pandaTakeDamageFromWater = getBoolean("mobs.panda.takes-damage-from-water", pandaTakeDamageFromWater); ++ pandaAlwaysDropExp = getBoolean("mobs.panda.always-drop-exp", pandaAlwaysDropExp); ++ } ++ ++ public boolean parrotRidable = false; ++ public boolean parrotRidableInWater = true; ++ public boolean parrotControllable = true; ++ public double parrotMaxY = 320D; ++ public double parrotMaxHealth = 6.0D; ++ public boolean parrotTakeDamageFromWater = false; ++ public boolean parrotBreedable = false; ++ public boolean parrotAlwaysDropExp = false; ++ private void parrotSettings() { ++ parrotRidable = getBoolean("mobs.parrot.ridable", parrotRidable); ++ parrotRidableInWater = getBoolean("mobs.parrot.ridable-in-water", parrotRidableInWater); ++ parrotControllable = getBoolean("mobs.parrot.controllable", parrotControllable); ++ parrotMaxY = getDouble("mobs.parrot.ridable-max-y", parrotMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.parrot.attributes.max-health", parrotMaxHealth); ++ set("mobs.parrot.attributes.max-health", null); ++ set("mobs.parrot.attributes.max_health", oldValue); ++ } ++ parrotMaxHealth = getDouble("mobs.parrot.attributes.max_health", parrotMaxHealth); ++ parrotTakeDamageFromWater = getBoolean("mobs.parrot.takes-damage-from-water", parrotTakeDamageFromWater); ++ parrotBreedable = getBoolean("mobs.parrot.can-breed", parrotBreedable); ++ parrotAlwaysDropExp = getBoolean("mobs.parrot.always-drop-exp", parrotAlwaysDropExp); ++ } ++ ++ public boolean phantomRidable = false; ++ public boolean phantomRidableInWater = true; ++ public boolean phantomControllable = true; ++ public double phantomMaxY = 320D; ++ public float phantomFlameDamage = 1.0F; ++ public int phantomFlameFireTime = 8; ++ public boolean phantomAllowGriefing = false; ++ public String phantomMaxHealth = "20.0"; ++ public String phantomAttackDamage = "6 + size"; ++ public Map phantomMaxHealthCache = new HashMap<>(); ++ public Map phantomAttackDamageCache = new HashMap<>(); ++ public double phantomAttackedByCrystalRadius = 0.0D; ++ public float phantomAttackedByCrystalDamage = 1.0F; ++ public double phantomOrbitCrystalRadius = 0.0D; ++ public int phantomSpawnMinSkyDarkness = 5; ++ public boolean phantomSpawnOnlyAboveSeaLevel = true; ++ public boolean phantomSpawnOnlyWithVisibleSky = true; ++ public double phantomSpawnLocalDifficultyChance = 3.0D; ++ public int phantomSpawnMinPerAttempt = 1; ++ public int phantomSpawnMaxPerAttempt = -1; ++ public int phantomBurnInLight = 0; ++ public boolean phantomIgnorePlayersWithTorch = false; ++ public boolean phantomBurnInDaylight = true; ++ public boolean phantomFlamesOnSwoop = false; ++ public boolean phantomTakeDamageFromWater = false; ++ public boolean phantomAlwaysDropExp = false; ++ public int phantomMinSize = 0; ++ public int phantomMaxSize = 0; ++ private void phantomSettings() { ++ phantomRidable = getBoolean("mobs.phantom.ridable", phantomRidable); ++ phantomRidableInWater = getBoolean("mobs.phantom.ridable-in-water", phantomRidableInWater); ++ phantomControllable = getBoolean("mobs.phantom.controllable", phantomControllable); ++ phantomMaxY = getDouble("mobs.phantom.ridable-max-y", phantomMaxY); ++ phantomFlameDamage = (float) getDouble("mobs.phantom.flames.damage", phantomFlameDamage); ++ phantomFlameFireTime = getInt("mobs.phantom.flames.fire-time", phantomFlameFireTime); ++ phantomAllowGriefing = getBoolean("mobs.phantom.allow-griefing", phantomAllowGriefing); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.phantom.attributes.max-health", Double.parseDouble(phantomMaxHealth)); ++ set("mobs.phantom.attributes.max-health", null); ++ set("mobs.phantom.attributes.max_health", String.valueOf(oldValue)); ++ } ++ if (PurpurConfig.version < 25) { ++ double oldValue = getDouble("mobs.phantom.attributes.max_health", Double.parseDouble(phantomMaxHealth)); ++ set("mobs.phantom.attributes.max_health", String.valueOf(oldValue)); ++ } ++ phantomMaxHealth = getString("mobs.phantom.attributes.max_health", phantomMaxHealth); ++ phantomAttackDamage = getString("mobs.phantom.attributes.attack_damage", phantomAttackDamage); ++ phantomMaxHealthCache.clear(); ++ phantomAttackDamageCache.clear(); ++ phantomAttackedByCrystalRadius = getDouble("mobs.phantom.attacked-by-crystal-range", phantomAttackedByCrystalRadius); ++ phantomAttackedByCrystalDamage = (float) getDouble("mobs.phantom.attacked-by-crystal-damage", phantomAttackedByCrystalDamage); ++ phantomOrbitCrystalRadius = getDouble("mobs.phantom.orbit-crystal-radius", phantomOrbitCrystalRadius); ++ phantomSpawnMinSkyDarkness = getInt("mobs.phantom.spawn.min-sky-darkness", phantomSpawnMinSkyDarkness); ++ phantomSpawnOnlyAboveSeaLevel = getBoolean("mobs.phantom.spawn.only-above-sea-level", phantomSpawnOnlyAboveSeaLevel); ++ phantomSpawnOnlyWithVisibleSky = getBoolean("mobs.phantom.spawn.only-with-visible-sky", phantomSpawnOnlyWithVisibleSky); ++ phantomSpawnLocalDifficultyChance = getDouble("mobs.phantom.spawn.local-difficulty-chance", phantomSpawnLocalDifficultyChance); ++ phantomSpawnMinPerAttempt = getInt("mobs.phantom.spawn.per-attempt.min", phantomSpawnMinPerAttempt); ++ phantomSpawnMaxPerAttempt = getInt("mobs.phantom.spawn.per-attempt.max", phantomSpawnMaxPerAttempt); ++ phantomBurnInLight = getInt("mobs.phantom.burn-in-light", phantomBurnInLight); ++ phantomBurnInDaylight = getBoolean("mobs.phantom.burn-in-daylight", phantomBurnInDaylight); ++ phantomIgnorePlayersWithTorch = getBoolean("mobs.phantom.ignore-players-with-torch", phantomIgnorePlayersWithTorch); ++ phantomFlamesOnSwoop = getBoolean("mobs.phantom.flames-on-swoop", phantomFlamesOnSwoop); ++ phantomTakeDamageFromWater = getBoolean("mobs.phantom.takes-damage-from-water", phantomTakeDamageFromWater); ++ phantomAlwaysDropExp = getBoolean("mobs.phantom.always-drop-exp", phantomAlwaysDropExp); ++ phantomMinSize = Mth.clamp(getInt("mobs.phantom.size.min", phantomMinSize), 0, 64); ++ phantomMaxSize = Mth.clamp(getInt("mobs.phantom.size.max", phantomMaxSize), 0, 64); ++ if (phantomMinSize > phantomMaxSize) { ++ phantomMinSize = phantomMinSize ^ phantomMaxSize; ++ phantomMaxSize = phantomMinSize ^ phantomMaxSize; ++ phantomMinSize = phantomMinSize ^ phantomMaxSize; ++ } ++ } ++ ++ public boolean pigRidable = false; ++ public boolean pigRidableInWater = false; ++ public boolean pigControllable = true; ++ public double pigMaxHealth = 10.0D; ++ public boolean pigGiveSaddleBack = false; ++ public int pigBreedingTicks = 6000; ++ public boolean pigTakeDamageFromWater = false; ++ public boolean pigAlwaysDropExp = false; ++ private void pigSettings() { ++ pigRidable = getBoolean("mobs.pig.ridable", pigRidable); ++ pigRidableInWater = getBoolean("mobs.pig.ridable-in-water", pigRidableInWater); ++ pigControllable = getBoolean("mobs.pig.controllable", pigControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pig.attributes.max-health", pigMaxHealth); ++ set("mobs.pig.attributes.max-health", null); ++ set("mobs.pig.attributes.max_health", oldValue); ++ } ++ pigMaxHealth = getDouble("mobs.pig.attributes.max_health", pigMaxHealth); ++ pigGiveSaddleBack = getBoolean("mobs.pig.give-saddle-back", pigGiveSaddleBack); ++ pigBreedingTicks = getInt("mobs.pig.breeding-delay-ticks", pigBreedingTicks); ++ pigTakeDamageFromWater = getBoolean("mobs.pig.takes-damage-from-water", pigTakeDamageFromWater); ++ pigAlwaysDropExp = getBoolean("mobs.pig.always-drop-exp", pigAlwaysDropExp); ++ } ++ ++ public boolean piglinRidable = false; ++ public boolean piglinRidableInWater = true; ++ public boolean piglinControllable = true; ++ public double piglinMaxHealth = 16.0D; ++ public boolean piglinBypassMobGriefing = false; ++ public boolean piglinTakeDamageFromWater = false; ++ public int piglinPortalSpawnModifier = 2000; ++ public boolean piglinAlwaysDropExp = false; ++ public double piglinHeadVisibilityPercent = 0.5D; ++ private void piglinSettings() { ++ piglinRidable = getBoolean("mobs.piglin.ridable", piglinRidable); ++ piglinRidableInWater = getBoolean("mobs.piglin.ridable-in-water", piglinRidableInWater); ++ piglinControllable = getBoolean("mobs.piglin.controllable", piglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.piglin.attributes.max-health", piglinMaxHealth); ++ set("mobs.piglin.attributes.max-health", null); ++ set("mobs.piglin.attributes.max_health", oldValue); ++ } ++ piglinMaxHealth = getDouble("mobs.piglin.attributes.max_health", piglinMaxHealth); ++ piglinBypassMobGriefing = getBoolean("mobs.piglin.bypass-mob-griefing", piglinBypassMobGriefing); ++ piglinTakeDamageFromWater = getBoolean("mobs.piglin.takes-damage-from-water", piglinTakeDamageFromWater); ++ piglinPortalSpawnModifier = getInt("mobs.piglin.portal-spawn-modifier", piglinPortalSpawnModifier); ++ piglinAlwaysDropExp = getBoolean("mobs.piglin.always-drop-exp", piglinAlwaysDropExp); ++ piglinHeadVisibilityPercent = getDouble("mobs.piglin.head-visibility-percent", piglinHeadVisibilityPercent); ++ } ++ ++ public boolean piglinBruteRidable = false; ++ public boolean piglinBruteRidableInWater = true; ++ public boolean piglinBruteControllable = true; ++ public double piglinBruteMaxHealth = 50.0D; ++ public boolean piglinBruteTakeDamageFromWater = false; ++ public boolean piglinBruteAlwaysDropExp = false; ++ private void piglinBruteSettings() { ++ piglinBruteRidable = getBoolean("mobs.piglin_brute.ridable", piglinBruteRidable); ++ piglinBruteRidableInWater = getBoolean("mobs.piglin_brute.ridable-in-water", piglinBruteRidableInWater); ++ piglinBruteControllable = getBoolean("mobs.piglin_brute.controllable", piglinBruteControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.piglin_brute.attributes.max-health", piglinBruteMaxHealth); ++ set("mobs.piglin_brute.attributes.max-health", null); ++ set("mobs.piglin_brute.attributes.max_health", oldValue); ++ } ++ piglinBruteMaxHealth = getDouble("mobs.piglin_brute.attributes.max_health", piglinBruteMaxHealth); ++ piglinBruteTakeDamageFromWater = getBoolean("mobs.piglin_brute.takes-damage-from-water", piglinBruteTakeDamageFromWater); ++ piglinBruteAlwaysDropExp = getBoolean("mobs.piglin_brute.always-drop-exp", piglinBruteAlwaysDropExp); ++ } ++ ++ public boolean pillagerRidable = false; ++ public boolean pillagerRidableInWater = true; ++ public boolean pillagerControllable = true; ++ public double pillagerMaxHealth = 24.0D; ++ public boolean pillagerBypassMobGriefing = false; ++ public boolean pillagerTakeDamageFromWater = false; ++ public boolean pillagerAlwaysDropExp = false; ++ private void pillagerSettings() { ++ pillagerRidable = getBoolean("mobs.pillager.ridable", pillagerRidable); ++ pillagerRidableInWater = getBoolean("mobs.pillager.ridable-in-water", pillagerRidableInWater); ++ pillagerControllable = getBoolean("mobs.pillager.controllable", pillagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pillager.attributes.max-health", pillagerMaxHealth); ++ set("mobs.pillager.attributes.max-health", null); ++ set("mobs.pillager.attributes.max_health", oldValue); ++ } ++ pillagerMaxHealth = getDouble("mobs.pillager.attributes.max_health", pillagerMaxHealth); ++ pillagerBypassMobGriefing = getBoolean("mobs.pillager.bypass-mob-griefing", pillagerBypassMobGriefing); ++ pillagerTakeDamageFromWater = getBoolean("mobs.pillager.takes-damage-from-water", pillagerTakeDamageFromWater); ++ pillagerAlwaysDropExp = getBoolean("mobs.pillager.always-drop-exp", pillagerAlwaysDropExp); ++ } ++ ++ public boolean polarBearRidable = false; ++ public boolean polarBearRidableInWater = true; ++ public boolean polarBearControllable = true; ++ public double polarBearMaxHealth = 30.0D; ++ public String polarBearBreedableItemString = ""; ++ public Item polarBearBreedableItem = null; ++ public int polarBearBreedingTicks = 6000; ++ public boolean polarBearTakeDamageFromWater = false; ++ public boolean polarBearAlwaysDropExp = false; ++ private void polarBearSettings() { ++ polarBearRidable = getBoolean("mobs.polar_bear.ridable", polarBearRidable); ++ polarBearRidableInWater = getBoolean("mobs.polar_bear.ridable-in-water", polarBearRidableInWater); ++ polarBearControllable = getBoolean("mobs.polar_bear.controllable", polarBearControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.polar_bear.attributes.max-health", polarBearMaxHealth); ++ set("mobs.polar_bear.attributes.max-health", null); ++ set("mobs.polar_bear.attributes.max_health", oldValue); ++ } ++ polarBearMaxHealth = getDouble("mobs.polar_bear.attributes.max_health", polarBearMaxHealth); ++ polarBearBreedableItemString = getString("mobs.polar_bear.breedable-item", polarBearBreedableItemString); ++ Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(polarBearBreedableItemString)); ++ if (item != Items.AIR) polarBearBreedableItem = item; ++ polarBearBreedingTicks = getInt("mobs.polar_bear.breeding-delay-ticks", polarBearBreedingTicks); ++ polarBearTakeDamageFromWater = getBoolean("mobs.polar_bear.takes-damage-from-water", polarBearTakeDamageFromWater); ++ polarBearAlwaysDropExp = getBoolean("mobs.polar_bear.always-drop-exp", polarBearAlwaysDropExp); ++ } ++ ++ public boolean pufferfishRidable = false; ++ public boolean pufferfishControllable = true; ++ public double pufferfishMaxHealth = 3.0D; ++ public boolean pufferfishTakeDamageFromWater = false; ++ public boolean pufferfishAlwaysDropExp = false; ++ private void pufferfishSettings() { ++ pufferfishRidable = getBoolean("mobs.pufferfish.ridable", pufferfishRidable); ++ pufferfishControllable = getBoolean("mobs.pufferfish.controllable", pufferfishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.pufferfish.attributes.max-health", pufferfishMaxHealth); ++ set("mobs.pufferfish.attributes.max-health", null); ++ set("mobs.pufferfish.attributes.max_health", oldValue); ++ } ++ pufferfishMaxHealth = getDouble("mobs.pufferfish.attributes.max_health", pufferfishMaxHealth); ++ pufferfishTakeDamageFromWater = getBoolean("mobs.pufferfish.takes-damage-from-water", pufferfishTakeDamageFromWater); ++ pufferfishAlwaysDropExp = getBoolean("mobs.pufferfish.always-drop-exp", pufferfishAlwaysDropExp); ++ } ++ ++ public boolean rabbitRidable = false; ++ public boolean rabbitRidableInWater = true; ++ public boolean rabbitControllable = true; ++ public double rabbitMaxHealth = 3.0D; ++ public double rabbitNaturalToast = 0.0D; ++ public double rabbitNaturalKiller = 0.0D; ++ public int rabbitBreedingTicks = 6000; ++ public boolean rabbitBypassMobGriefing = false; ++ public boolean rabbitTakeDamageFromWater = false; ++ public boolean rabbitAlwaysDropExp = false; ++ private void rabbitSettings() { ++ rabbitRidable = getBoolean("mobs.rabbit.ridable", rabbitRidable); ++ rabbitRidableInWater = getBoolean("mobs.rabbit.ridable-in-water", rabbitRidableInWater); ++ rabbitControllable = getBoolean("mobs.rabbit.controllable", rabbitControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.rabbit.attributes.max-health", rabbitMaxHealth); ++ set("mobs.rabbit.attributes.max-health", null); ++ set("mobs.rabbit.attributes.max_health", oldValue); ++ } ++ rabbitMaxHealth = getDouble("mobs.rabbit.attributes.max_health", rabbitMaxHealth); ++ rabbitNaturalToast = getDouble("mobs.rabbit.spawn-toast-chance", rabbitNaturalToast); ++ rabbitNaturalKiller = getDouble("mobs.rabbit.spawn-killer-rabbit-chance", rabbitNaturalKiller); ++ rabbitBreedingTicks = getInt("mobs.rabbit.breeding-delay-ticks", rabbitBreedingTicks); ++ rabbitBypassMobGriefing = getBoolean("mobs.rabbit.bypass-mob-griefing", rabbitBypassMobGriefing); ++ rabbitTakeDamageFromWater = getBoolean("mobs.rabbit.takes-damage-from-water", rabbitTakeDamageFromWater); ++ rabbitAlwaysDropExp = getBoolean("mobs.rabbit.always-drop-exp", rabbitAlwaysDropExp); ++ } ++ ++ public boolean ravagerRidable = false; ++ public boolean ravagerRidableInWater = false; ++ public boolean ravagerControllable = true; ++ public double ravagerMaxHealth = 100.0D; ++ public boolean ravagerBypassMobGriefing = false; ++ public boolean ravagerTakeDamageFromWater = false; ++ public List ravagerGriefableBlocks = new ArrayList<>(); ++ public boolean ravagerAlwaysDropExp = false; ++ private void ravagerSettings() { ++ ravagerRidable = getBoolean("mobs.ravager.ridable", ravagerRidable); ++ ravagerRidableInWater = getBoolean("mobs.ravager.ridable-in-water", ravagerRidableInWater); ++ ravagerControllable = getBoolean("mobs.ravager.controllable", ravagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.ravager.attributes.max-health", ravagerMaxHealth); ++ set("mobs.ravager.attributes.max-health", null); ++ set("mobs.ravager.attributes.max_health", oldValue); ++ } ++ ravagerMaxHealth = getDouble("mobs.ravager.attributes.max_health", ravagerMaxHealth); ++ ravagerBypassMobGriefing = getBoolean("mobs.ravager.bypass-mob-griefing", ravagerBypassMobGriefing); ++ ravagerTakeDamageFromWater = getBoolean("mobs.ravager.takes-damage-from-water", ravagerTakeDamageFromWater); ++ getList("mobs.ravager.griefable-blocks", new ArrayList(){{ ++ add("minecraft:oak_leaves"); ++ add("minecraft:spruce_leaves"); ++ add("minecraft:birch_leaves"); ++ add("minecraft:jungle_leaves"); ++ add("minecraft:acacia_leaves"); ++ add("minecraft:dark_oak_leaves"); ++ add("minecraft:beetroots"); ++ add("minecraft:carrots"); ++ add("minecraft:potatoes"); ++ add("minecraft:wheat"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ ravagerGriefableBlocks.add(block); ++ } ++ }); ++ ravagerAlwaysDropExp = getBoolean("mobs.ravager.always-drop-exp", ravagerAlwaysDropExp); ++ } ++ ++ public boolean salmonRidable = false; ++ public boolean salmonControllable = true; ++ public double salmonMaxHealth = 3.0D; ++ public boolean salmonTakeDamageFromWater = false; ++ public boolean salmonAlwaysDropExp = false; ++ private void salmonSettings() { ++ salmonRidable = getBoolean("mobs.salmon.ridable", salmonRidable); ++ salmonControllable = getBoolean("mobs.salmon.controllable", salmonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.salmon.attributes.max-health", salmonMaxHealth); ++ set("mobs.salmon.attributes.max-health", null); ++ set("mobs.salmon.attributes.max_health", oldValue); ++ } ++ salmonMaxHealth = getDouble("mobs.salmon.attributes.max_health", salmonMaxHealth); ++ salmonTakeDamageFromWater = getBoolean("mobs.salmon.takes-damage-from-water", salmonTakeDamageFromWater); ++ salmonAlwaysDropExp = getBoolean("mobs.salmon.always-drop-exp", salmonAlwaysDropExp); ++ } ++ ++ public boolean sheepRidable = false; ++ public boolean sheepRidableInWater = true; ++ public boolean sheepControllable = true; ++ public double sheepMaxHealth = 8.0D; ++ public int sheepBreedingTicks = 6000; ++ public boolean sheepBypassMobGriefing = false; ++ public boolean sheepTakeDamageFromWater = false; ++ public boolean sheepAlwaysDropExp = false; ++ public boolean sheepShearJebRandomColor = false; ++ private void sheepSettings() { ++ sheepRidable = getBoolean("mobs.sheep.ridable", sheepRidable); ++ sheepRidableInWater = getBoolean("mobs.sheep.ridable-in-water", sheepRidableInWater); ++ sheepControllable = getBoolean("mobs.sheep.controllable", sheepControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.sheep.attributes.max-health", sheepMaxHealth); ++ set("mobs.sheep.attributes.max-health", null); ++ set("mobs.sheep.attributes.max_health", oldValue); ++ } ++ sheepMaxHealth = getDouble("mobs.sheep.attributes.max_health", sheepMaxHealth); ++ sheepBreedingTicks = getInt("mobs.sheep.breeding-delay-ticks", sheepBreedingTicks); ++ sheepBypassMobGriefing = getBoolean("mobs.sheep.bypass-mob-griefing", sheepBypassMobGriefing); ++ sheepTakeDamageFromWater = getBoolean("mobs.sheep.takes-damage-from-water", sheepTakeDamageFromWater); ++ sheepAlwaysDropExp = getBoolean("mobs.sheep.always-drop-exp", sheepAlwaysDropExp); ++ sheepShearJebRandomColor = getBoolean("mobs.sheep.jeb-shear-random-color", sheepShearJebRandomColor); ++ } ++ ++ public boolean shulkerRidable = false; ++ public boolean shulkerRidableInWater = true; ++ public boolean shulkerControllable = true; ++ public double shulkerMaxHealth = 30.0D; ++ public boolean shulkerTakeDamageFromWater = false; ++ public float shulkerSpawnFromBulletBaseChance = 1.0F; ++ public boolean shulkerSpawnFromBulletRequireOpenLid = true; ++ public double shulkerSpawnFromBulletNearbyRange = 8.0D; ++ public String shulkerSpawnFromBulletNearbyEquation = "(nearby - 1) / 5.0"; ++ public boolean shulkerSpawnFromBulletRandomColor = false; ++ public boolean shulkerChangeColorWithDye = false; ++ public boolean shulkerAlwaysDropExp = false; ++ private void shulkerSettings() { ++ shulkerRidable = getBoolean("mobs.shulker.ridable", shulkerRidable); ++ shulkerRidableInWater = getBoolean("mobs.shulker.ridable-in-water", shulkerRidableInWater); ++ shulkerControllable = getBoolean("mobs.shulker.controllable", shulkerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.shulker.attributes.max-health", shulkerMaxHealth); ++ set("mobs.shulker.attributes.max-health", null); ++ set("mobs.shulker.attributes.max_health", oldValue); ++ } ++ shulkerMaxHealth = getDouble("mobs.shulker.attributes.max_health", shulkerMaxHealth); ++ shulkerTakeDamageFromWater = getBoolean("mobs.shulker.takes-damage-from-water", shulkerTakeDamageFromWater); ++ shulkerSpawnFromBulletBaseChance = (float) getDouble("mobs.shulker.spawn-from-bullet.base-chance", shulkerSpawnFromBulletBaseChance); ++ shulkerSpawnFromBulletRequireOpenLid = getBoolean("mobs.shulker.spawn-from-bullet.require-open-lid", shulkerSpawnFromBulletRequireOpenLid); ++ shulkerSpawnFromBulletNearbyRange = getDouble("mobs.shulker.spawn-from-bullet.nearby-range", shulkerSpawnFromBulletNearbyRange); ++ shulkerSpawnFromBulletNearbyEquation = getString("mobs.shulker.spawn-from-bullet.nearby-equation", shulkerSpawnFromBulletNearbyEquation); ++ shulkerSpawnFromBulletRandomColor = getBoolean("mobs.shulker.spawn-from-bullet.random-color", shulkerSpawnFromBulletRandomColor); ++ shulkerChangeColorWithDye = getBoolean("mobs.shulker.change-color-with-dye", shulkerChangeColorWithDye); ++ shulkerAlwaysDropExp = getBoolean("mobs.shulker.always-drop-exp", shulkerAlwaysDropExp); ++ } ++ ++ public boolean silverfishRidable = false; ++ public boolean silverfishRidableInWater = true; ++ public boolean silverfishControllable = true; ++ public double silverfishMaxHealth = 8.0D; ++ public boolean silverfishBypassMobGriefing = false; ++ public boolean silverfishTakeDamageFromWater = false; ++ public boolean silverfishAlwaysDropExp = false; ++ private void silverfishSettings() { ++ silverfishRidable = getBoolean("mobs.silverfish.ridable", silverfishRidable); ++ silverfishRidableInWater = getBoolean("mobs.silverfish.ridable-in-water", silverfishRidableInWater); ++ silverfishControllable = getBoolean("mobs.silverfish.controllable", silverfishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.silverfish.attributes.max-health", silverfishMaxHealth); ++ set("mobs.silverfish.attributes.max-health", null); ++ set("mobs.silverfish.attributes.max_health", oldValue); ++ } ++ silverfishMaxHealth = getDouble("mobs.silverfish.attributes.max_health", silverfishMaxHealth); ++ silverfishBypassMobGriefing = getBoolean("mobs.silverfish.bypass-mob-griefing", silverfishBypassMobGriefing); ++ silverfishTakeDamageFromWater = getBoolean("mobs.silverfish.takes-damage-from-water", silverfishTakeDamageFromWater); ++ silverfishAlwaysDropExp = getBoolean("mobs.silverfish.always-drop-exp", silverfishAlwaysDropExp); ++ } ++ ++ public boolean skeletonRidable = false; ++ public boolean skeletonRidableInWater = true; ++ public boolean skeletonControllable = true; ++ public double skeletonMaxHealth = 20.0D; ++ public boolean skeletonTakeDamageFromWater = false; ++ public boolean skeletonAlwaysDropExp = false; ++ public double skeletonHeadVisibilityPercent = 0.5D; ++ public int skeletonFeedWitherRoses = 0; ++ public String skeletonBowAccuracy = "14 - difficulty * 4"; ++ public Map skeletonBowAccuracyMap = new HashMap<>(); ++ private void skeletonSettings() { ++ skeletonRidable = getBoolean("mobs.skeleton.ridable", skeletonRidable); ++ skeletonRidableInWater = getBoolean("mobs.skeleton.ridable-in-water", skeletonRidableInWater); ++ skeletonControllable = getBoolean("mobs.skeleton.controllable", skeletonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.skeleton.attributes.max-health", skeletonMaxHealth); ++ set("mobs.skeleton.attributes.max-health", null); ++ set("mobs.skeleton.attributes.max_health", oldValue); ++ } ++ skeletonMaxHealth = getDouble("mobs.skeleton.attributes.max_health", skeletonMaxHealth); ++ skeletonTakeDamageFromWater = getBoolean("mobs.skeleton.takes-damage-from-water", skeletonTakeDamageFromWater); ++ skeletonAlwaysDropExp = getBoolean("mobs.skeleton.always-drop-exp", skeletonAlwaysDropExp); ++ skeletonHeadVisibilityPercent = getDouble("mobs.skeleton.head-visibility-percent", skeletonHeadVisibilityPercent); ++ skeletonFeedWitherRoses = getInt("mobs.skeleton.feed-wither-roses", skeletonFeedWitherRoses); ++ final String defaultSkeletonBowAccuracy = skeletonBowAccuracy; ++ skeletonBowAccuracy = getString("mobs.skeleton.bow-accuracy", skeletonBowAccuracy); ++ for (int i = 1; i < 4; i++) { ++ final float divergence; ++ try { ++ divergence = ((Number) Entity.scriptEngine.eval("let difficulty = " + i + "; " + skeletonBowAccuracy)).floatValue(); ++ } catch (javax.script.ScriptException e) { ++ e.printStackTrace(); ++ break; ++ } ++ skeletonBowAccuracyMap.put(i, divergence); ++ } ++ } ++ ++ public boolean skeletonHorseRidableInWater = true; ++ public boolean skeletonHorseCanSwim = false; ++ public double skeletonHorseMaxHealthMin = 15.0D; ++ public double skeletonHorseMaxHealthMax = 15.0D; ++ public double skeletonHorseJumpStrengthMin = 0.4D; ++ public double skeletonHorseJumpStrengthMax = 1.0D; ++ public double skeletonHorseMovementSpeedMin = 0.2D; ++ public double skeletonHorseMovementSpeedMax = 0.2D; ++ public boolean skeletonHorseTakeDamageFromWater = false; ++ public boolean skeletonHorseAlwaysDropExp = false; ++ private void skeletonHorseSettings() { ++ skeletonHorseRidableInWater = getBoolean("mobs.skeleton_horse.ridable-in-water", skeletonHorseRidableInWater); ++ skeletonHorseCanSwim = getBoolean("mobs.skeleton_horse.can-swim", skeletonHorseCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.skeleton_horse.attributes.max-health", skeletonHorseMaxHealthMin); ++ set("mobs.skeleton_horse.attributes.max-health", null); ++ set("mobs.skeleton_horse.attributes.max_health.min", oldValue); ++ set("mobs.skeleton_horse.attributes.max_health.max", oldValue); ++ } ++ skeletonHorseMaxHealthMin = getDouble("mobs.skeleton_horse.attributes.max_health.min", skeletonHorseMaxHealthMin); ++ skeletonHorseMaxHealthMax = getDouble("mobs.skeleton_horse.attributes.max_health.max", skeletonHorseMaxHealthMax); ++ skeletonHorseJumpStrengthMin = getDouble("mobs.skeleton_horse.attributes.jump_strength.min", skeletonHorseJumpStrengthMin); ++ skeletonHorseJumpStrengthMax = getDouble("mobs.skeleton_horse.attributes.jump_strength.max", skeletonHorseJumpStrengthMax); ++ skeletonHorseMovementSpeedMin = getDouble("mobs.skeleton_horse.attributes.movement_speed.min", skeletonHorseMovementSpeedMin); ++ skeletonHorseMovementSpeedMax = getDouble("mobs.skeleton_horse.attributes.movement_speed.max", skeletonHorseMovementSpeedMax); ++ skeletonHorseTakeDamageFromWater = getBoolean("mobs.skeleton_horse.takes-damage-from-water", skeletonHorseTakeDamageFromWater); ++ skeletonHorseAlwaysDropExp = getBoolean("mobs.skeleton_horse.always-drop-exp", skeletonHorseAlwaysDropExp); ++ } ++ ++ public boolean slimeRidable = false; ++ public boolean slimeRidableInWater = true; ++ public boolean slimeControllable = true; ++ public String slimeMaxHealth = "size * size"; ++ public String slimeAttackDamage = "size"; ++ public Map slimeMaxHealthCache = new HashMap<>(); ++ public Map slimeAttackDamageCache = new HashMap<>(); ++ public boolean slimeTakeDamageFromWater = false; ++ public boolean slimeAlwaysDropExp = false; ++ private void slimeSettings() { ++ slimeRidable = getBoolean("mobs.slime.ridable", slimeRidable); ++ slimeRidableInWater = getBoolean("mobs.slime.ridable-in-water", slimeRidableInWater); ++ slimeControllable = getBoolean("mobs.slime.controllable", slimeControllable); ++ if (PurpurConfig.version < 10) { ++ String oldValue = getString("mobs.slime.attributes.max-health", slimeMaxHealth); ++ set("mobs.slime.attributes.max-health", null); ++ set("mobs.slime.attributes.max_health", oldValue); ++ } ++ slimeMaxHealth = getString("mobs.slime.attributes.max_health", slimeMaxHealth); ++ slimeAttackDamage = getString("mobs.slime.attributes.attack_damage", slimeAttackDamage); ++ slimeMaxHealthCache.clear(); ++ slimeAttackDamageCache.clear(); ++ slimeTakeDamageFromWater = getBoolean("mobs.slime.takes-damage-from-water", slimeTakeDamageFromWater); ++ slimeAlwaysDropExp = getBoolean("mobs.slime.always-drop-exp", slimeAlwaysDropExp); ++ } ++ ++ public boolean snowGolemRidable = false; ++ public boolean snowGolemRidableInWater = true; ++ public boolean snowGolemControllable = true; ++ public boolean snowGolemLeaveTrailWhenRidden = false; ++ public double snowGolemMaxHealth = 4.0D; ++ public boolean snowGolemDropsPumpkin = true; ++ public boolean snowGolemPutPumpkinBack = false; ++ public int snowGolemSnowBallMin = 20; ++ public int snowGolemSnowBallMax = 20; ++ public float snowGolemSnowBallModifier = 10.0F; ++ public double snowGolemAttackDistance = 1.25D; ++ public boolean snowGolemBypassMobGriefing = false; ++ public boolean snowGolemTakeDamageFromWater = true; ++ public boolean snowGolemAlwaysDropExp = false; ++ private void snowGolemSettings() { ++ snowGolemRidable = getBoolean("mobs.snow_golem.ridable", snowGolemRidable); ++ snowGolemRidableInWater = getBoolean("mobs.snow_golem.ridable-in-water", snowGolemRidableInWater); ++ snowGolemControllable = getBoolean("mobs.snow_golem.controllable", snowGolemControllable); ++ snowGolemLeaveTrailWhenRidden = getBoolean("mobs.snow_golem.leave-trail-when-ridden", snowGolemLeaveTrailWhenRidden); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.snow_golem.attributes.max-health", snowGolemMaxHealth); ++ set("mobs.snow_golem.attributes.max-health", null); ++ set("mobs.snow_golem.attributes.max_health", oldValue); ++ } ++ snowGolemMaxHealth = getDouble("mobs.snow_golem.attributes.max_health", snowGolemMaxHealth); ++ snowGolemDropsPumpkin = getBoolean("mobs.snow_golem.drop-pumpkin-when-sheared", snowGolemDropsPumpkin); ++ snowGolemPutPumpkinBack = getBoolean("mobs.snow_golem.pumpkin-can-be-added-back", snowGolemPutPumpkinBack); ++ snowGolemSnowBallMin = getInt("mobs.snow_golem.min-shoot-interval-ticks", snowGolemSnowBallMin); ++ snowGolemSnowBallMax = getInt("mobs.snow_golem.max-shoot-interval-ticks", snowGolemSnowBallMax); ++ snowGolemSnowBallModifier = (float) getDouble("mobs.snow_golem.snow-ball-modifier", snowGolemSnowBallModifier); ++ snowGolemAttackDistance = getDouble("mobs.snow_golem.attack-distance", snowGolemAttackDistance); ++ snowGolemBypassMobGriefing = getBoolean("mobs.snow_golem.bypass-mob-griefing", snowGolemBypassMobGriefing); ++ snowGolemTakeDamageFromWater = getBoolean("mobs.snow_golem.takes-damage-from-water", snowGolemTakeDamageFromWater); ++ snowGolemAlwaysDropExp = getBoolean("mobs.snow_golem.always-drop-exp", snowGolemAlwaysDropExp); ++ } ++ ++ public boolean snifferRidable = false; ++ public boolean snifferRidableInWater = true; ++ public boolean snifferControllable = true; ++ public double snifferMaxHealth = 14.0D; ++ public int snifferBreedingTicks = 6000; ++ private void snifferSettings() { ++ snifferRidable = getBoolean("mobs.sniffer.ridable", snifferRidable); ++ snifferRidableInWater = getBoolean("mobs.sniffer.ridable-in-water", snifferRidableInWater); ++ snifferControllable = getBoolean("mobs.sniffer.controllable", snifferControllable); ++ snifferMaxHealth = getDouble("mobs.sniffer.attributes.max_health", snifferMaxHealth); ++ snifferBreedingTicks = getInt("mobs.sniffer.breeding-delay-ticks", chickenBreedingTicks); ++ } ++ ++ public boolean squidRidable = false; ++ public boolean squidControllable = true; ++ public double squidMaxHealth = 10.0D; ++ public boolean squidImmuneToEAR = true; ++ public double squidOffsetWaterCheck = 0.0D; ++ public boolean squidsCanFly = false; ++ public boolean squidTakeDamageFromWater = false; ++ public boolean squidAlwaysDropExp = false; ++ private void squidSettings() { ++ squidRidable = getBoolean("mobs.squid.ridable", squidRidable); ++ squidControllable = getBoolean("mobs.squid.controllable", squidControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.squid.attributes.max-health", squidMaxHealth); ++ set("mobs.squid.attributes.max-health", null); ++ set("mobs.squid.attributes.max_health", oldValue); ++ } ++ squidMaxHealth = getDouble("mobs.squid.attributes.max_health", squidMaxHealth); ++ squidImmuneToEAR = getBoolean("mobs.squid.immune-to-EAR", squidImmuneToEAR); ++ squidOffsetWaterCheck = getDouble("mobs.squid.water-offset-check", squidOffsetWaterCheck); ++ squidsCanFly = getBoolean("mobs.squid.can-fly", squidsCanFly); ++ squidTakeDamageFromWater = getBoolean("mobs.squid.takes-damage-from-water", squidTakeDamageFromWater); ++ squidAlwaysDropExp = getBoolean("mobs.squid.always-drop-exp", squidAlwaysDropExp); ++ } ++ ++ public boolean spiderRidable = false; ++ public boolean spiderRidableInWater = false; ++ public boolean spiderControllable = true; ++ public double spiderMaxHealth = 16.0D; ++ public boolean spiderTakeDamageFromWater = false; ++ public boolean spiderAlwaysDropExp = false; ++ private void spiderSettings() { ++ spiderRidable = getBoolean("mobs.spider.ridable", spiderRidable); ++ spiderRidableInWater = getBoolean("mobs.spider.ridable-in-water", spiderRidableInWater); ++ spiderControllable = getBoolean("mobs.spider.controllable", spiderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.spider.attributes.max-health", spiderMaxHealth); ++ set("mobs.spider.attributes.max-health", null); ++ set("mobs.spider.attributes.max_health", oldValue); ++ } ++ spiderMaxHealth = getDouble("mobs.spider.attributes.max_health", spiderMaxHealth); ++ spiderTakeDamageFromWater = getBoolean("mobs.spider.takes-damage-from-water", spiderTakeDamageFromWater); ++ spiderAlwaysDropExp = getBoolean("mobs.spider.always-drop-exp", spiderAlwaysDropExp); ++ } ++ ++ public boolean strayRidable = false; ++ public boolean strayRidableInWater = true; ++ public boolean strayControllable = true; ++ public double strayMaxHealth = 20.0D; ++ public boolean strayTakeDamageFromWater = false; ++ public boolean strayAlwaysDropExp = false; ++ private void straySettings() { ++ strayRidable = getBoolean("mobs.stray.ridable", strayRidable); ++ strayRidableInWater = getBoolean("mobs.stray.ridable-in-water", strayRidableInWater); ++ strayControllable = getBoolean("mobs.stray.controllable", strayControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.stray.attributes.max-health", strayMaxHealth); ++ set("mobs.stray.attributes.max-health", null); ++ set("mobs.stray.attributes.max_health", oldValue); ++ } ++ strayMaxHealth = getDouble("mobs.stray.attributes.max_health", strayMaxHealth); ++ strayTakeDamageFromWater = getBoolean("mobs.stray.takes-damage-from-water", strayTakeDamageFromWater); ++ strayAlwaysDropExp = getBoolean("mobs.stray.always-drop-exp", strayAlwaysDropExp); ++ } ++ ++ public boolean striderRidable = false; ++ public boolean striderRidableInWater = false; ++ public boolean striderControllable = true; ++ public double striderMaxHealth = 20.0D; ++ public int striderBreedingTicks = 6000; ++ public boolean striderGiveSaddleBack = false; ++ public boolean striderTakeDamageFromWater = true; ++ public boolean striderAlwaysDropExp = false; ++ private void striderSettings() { ++ striderRidable = getBoolean("mobs.strider.ridable", striderRidable); ++ striderRidableInWater = getBoolean("mobs.strider.ridable-in-water", striderRidableInWater); ++ striderControllable = getBoolean("mobs.strider.controllable", striderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.strider.attributes.max-health", striderMaxHealth); ++ set("mobs.strider.attributes.max-health", null); ++ set("mobs.strider.attributes.max_health", oldValue); ++ } ++ striderMaxHealth = getDouble("mobs.strider.attributes.max_health", striderMaxHealth); ++ striderBreedingTicks = getInt("mobs.strider.breeding-delay-ticks", striderBreedingTicks); ++ striderGiveSaddleBack = getBoolean("mobs.strider.give-saddle-back", striderGiveSaddleBack); ++ striderTakeDamageFromWater = getBoolean("mobs.strider.takes-damage-from-water", striderTakeDamageFromWater); ++ striderAlwaysDropExp = getBoolean("mobs.strider.always-drop-exp", striderAlwaysDropExp); ++ } ++ ++ public boolean tadpoleRidable = false; ++ public boolean tadpoleRidableInWater = true; ++ public boolean tadpoleControllable = true; ++ private void tadpoleSettings() { ++ tadpoleRidable = getBoolean("mobs.tadpole.ridable", tadpoleRidable); ++ tadpoleRidableInWater = getBoolean("mobs.tadpole.ridable-in-water", tadpoleRidableInWater); ++ tadpoleControllable = getBoolean("mobs.tadpole.controllable", tadpoleControllable); ++ } ++ ++ public boolean traderLlamaRidable = false; ++ public boolean traderLlamaRidableInWater = false; ++ public boolean traderLlamaControllable = true; ++ public double traderLlamaMaxHealthMin = 15.0D; ++ public double traderLlamaMaxHealthMax = 30.0D; ++ public double traderLlamaJumpStrengthMin = 0.5D; ++ public double traderLlamaJumpStrengthMax = 0.5D; ++ public double traderLlamaMovementSpeedMin = 0.175D; ++ public double traderLlamaMovementSpeedMax = 0.175D; ++ public int traderLlamaBreedingTicks = 6000; ++ public boolean traderLlamaTakeDamageFromWater = false; ++ public boolean traderLlamaAlwaysDropExp = false; ++ private void traderLlamaSettings() { ++ traderLlamaRidable = getBoolean("mobs.trader_llama.ridable", traderLlamaRidable); ++ traderLlamaRidableInWater = getBoolean("mobs.trader_llama.ridable-in-water", traderLlamaRidableInWater); ++ traderLlamaControllable = getBoolean("mobs.trader_llama.controllable", traderLlamaControllable); ++ if (PurpurConfig.version < 10) { ++ double oldMin = getDouble("mobs.trader_llama.attributes.max-health.min", traderLlamaMaxHealthMin); ++ double oldMax = getDouble("mobs.trader_llama.attributes.max-health.max", traderLlamaMaxHealthMax); ++ set("mobs.trader_llama.attributes.max-health", null); ++ set("mobs.trader_llama.attributes.max_health.min", oldMin); ++ set("mobs.trader_llama.attributes.max_health.max", oldMax); ++ } ++ traderLlamaMaxHealthMin = getDouble("mobs.trader_llama.attributes.max_health.min", traderLlamaMaxHealthMin); ++ traderLlamaMaxHealthMax = getDouble("mobs.trader_llama.attributes.max_health.max", traderLlamaMaxHealthMax); ++ traderLlamaJumpStrengthMin = getDouble("mobs.trader_llama.attributes.jump_strength.min", traderLlamaJumpStrengthMin); ++ traderLlamaJumpStrengthMax = getDouble("mobs.trader_llama.attributes.jump_strength.max", traderLlamaJumpStrengthMax); ++ traderLlamaMovementSpeedMin = getDouble("mobs.trader_llama.attributes.movement_speed.min", traderLlamaMovementSpeedMin); ++ traderLlamaMovementSpeedMax = getDouble("mobs.trader_llama.attributes.movement_speed.max", traderLlamaMovementSpeedMax); ++ traderLlamaBreedingTicks = getInt("mobs.trader_llama.breeding-delay-ticks", traderLlamaBreedingTicks); ++ traderLlamaTakeDamageFromWater = getBoolean("mobs.trader_llama.takes-damage-from-water", traderLlamaTakeDamageFromWater); ++ traderLlamaAlwaysDropExp = getBoolean("mobs.trader_llama.always-drop-exp", traderLlamaAlwaysDropExp); ++ } ++ ++ public boolean tropicalFishRidable = false; ++ public boolean tropicalFishControllable = true; ++ public double tropicalFishMaxHealth = 3.0D; ++ public boolean tropicalFishTakeDamageFromWater = false; ++ public boolean tropicalFishAlwaysDropExp = false; ++ private void tropicalFishSettings() { ++ tropicalFishRidable = getBoolean("mobs.tropical_fish.ridable", tropicalFishRidable); ++ tropicalFishControllable = getBoolean("mobs.tropical_fish.controllable", tropicalFishControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.tropical_fish.attributes.max-health", tropicalFishMaxHealth); ++ set("mobs.tropical_fish.attributes.max-health", null); ++ set("mobs.tropical_fish.attributes.max_health", oldValue); ++ } ++ tropicalFishMaxHealth = getDouble("mobs.tropical_fish.attributes.max_health", tropicalFishMaxHealth); ++ tropicalFishTakeDamageFromWater = getBoolean("mobs.tropical_fish.takes-damage-from-water", tropicalFishTakeDamageFromWater); ++ tropicalFishAlwaysDropExp = getBoolean("mobs.tropical_fish.always-drop-exp", tropicalFishAlwaysDropExp); ++ } ++ ++ public boolean turtleRidable = false; ++ public boolean turtleRidableInWater = true; ++ public boolean turtleControllable = true; ++ public double turtleMaxHealth = 30.0D; ++ public int turtleBreedingTicks = 6000; ++ public boolean turtleTakeDamageFromWater = false; ++ public boolean turtleAlwaysDropExp = false; ++ private void turtleSettings() { ++ turtleRidable = getBoolean("mobs.turtle.ridable", turtleRidable); ++ turtleRidableInWater = getBoolean("mobs.turtle.ridable-in-water", turtleRidableInWater); ++ turtleControllable = getBoolean("mobs.turtle.controllable", turtleControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.turtle.attributes.max-health", turtleMaxHealth); ++ set("mobs.turtle.attributes.max-health", null); ++ set("mobs.turtle.attributes.max_health", oldValue); ++ } ++ turtleMaxHealth = getDouble("mobs.turtle.attributes.max_health", turtleMaxHealth); ++ turtleBreedingTicks = getInt("mobs.turtle.breeding-delay-ticks", turtleBreedingTicks); ++ turtleTakeDamageFromWater = getBoolean("mobs.turtle.takes-damage-from-water", turtleTakeDamageFromWater); ++ turtleAlwaysDropExp = getBoolean("mobs.turtle.always-drop-exp", turtleAlwaysDropExp); ++ } ++ ++ public boolean vexRidable = false; ++ public boolean vexRidableInWater = true; ++ public boolean vexControllable = true; ++ public double vexMaxY = 320D; ++ public double vexMaxHealth = 14.0D; ++ public boolean vexTakeDamageFromWater = false; ++ public boolean vexAlwaysDropExp = false; ++ private void vexSettings() { ++ vexRidable = getBoolean("mobs.vex.ridable", vexRidable); ++ vexRidableInWater = getBoolean("mobs.vex.ridable-in-water", vexRidableInWater); ++ vexControllable = getBoolean("mobs.vex.controllable", vexControllable); ++ vexMaxY = getDouble("mobs.vex.ridable-max-y", vexMaxY); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.vex.attributes.max-health", vexMaxHealth); ++ set("mobs.vex.attributes.max-health", null); ++ set("mobs.vex.attributes.max_health", oldValue); ++ } ++ vexMaxHealth = getDouble("mobs.vex.attributes.max_health", vexMaxHealth); ++ vexTakeDamageFromWater = getBoolean("mobs.vex.takes-damage-from-water", vexTakeDamageFromWater); ++ vexAlwaysDropExp = getBoolean("mobs.vex.always-drop-exp", vexAlwaysDropExp); ++ } ++ ++ public boolean villagerRidable = false; ++ public boolean villagerRidableInWater = true; ++ public boolean villagerControllable = true; ++ public double villagerMaxHealth = 20.0D; ++ public boolean villagerFollowEmeraldBlock = false; ++ public boolean villagerCanBeLeashed = false; ++ public boolean villagerCanBreed = true; ++ public int villagerBreedingTicks = 6000; ++ public boolean villagerClericsFarmWarts = false; ++ public boolean villagerClericFarmersThrowWarts = true; ++ public boolean villagerBypassMobGriefing = false; ++ public boolean villagerTakeDamageFromWater = false; ++ public boolean villagerAllowTrading = true; ++ public boolean villagerAlwaysDropExp = false; ++ public int villagerMinimumDemand = 0; ++ public boolean villagerLobotomizeEnabled = false; ++ public int villagerLobotomizeCheckInterval = 100; ++ public boolean villagerDisplayTradeItem = true; ++ public int villagerSpawnIronGolemRadius = 0; ++ public int villagerSpawnIronGolemLimit = 0; ++ private void villagerSettings() { ++ villagerRidable = getBoolean("mobs.villager.ridable", villagerRidable); ++ villagerRidableInWater = getBoolean("mobs.villager.ridable-in-water", villagerRidableInWater); ++ villagerControllable = getBoolean("mobs.villager.controllable", villagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.villager.attributes.max-health", villagerMaxHealth); ++ set("mobs.villager.attributes.max-health", null); ++ set("mobs.villager.attributes.max_health", oldValue); ++ } ++ villagerMaxHealth = getDouble("mobs.villager.attributes.max_health", villagerMaxHealth); ++ villagerFollowEmeraldBlock = getBoolean("mobs.villager.follow-emerald-blocks", villagerFollowEmeraldBlock); ++ villagerCanBeLeashed = getBoolean("mobs.villager.can-be-leashed", villagerCanBeLeashed); ++ villagerCanBreed = getBoolean("mobs.villager.can-breed", villagerCanBreed); ++ villagerBreedingTicks = getInt("mobs.villager.breeding-delay-ticks", villagerBreedingTicks); ++ villagerClericsFarmWarts = getBoolean("mobs.villager.clerics-farm-warts", villagerClericsFarmWarts); ++ villagerClericFarmersThrowWarts = getBoolean("mobs.villager.cleric-wart-farmers-throw-warts-at-villagers", villagerClericFarmersThrowWarts); ++ villagerBypassMobGriefing = getBoolean("mobs.villager.bypass-mob-griefing", villagerBypassMobGriefing); ++ villagerTakeDamageFromWater = getBoolean("mobs.villager.takes-damage-from-water", villagerTakeDamageFromWater); ++ villagerAllowTrading = getBoolean("mobs.villager.allow-trading", villagerAllowTrading); ++ villagerAlwaysDropExp = getBoolean("mobs.villager.always-drop-exp", villagerAlwaysDropExp); ++ villagerMinimumDemand = getInt("mobs.villager.minimum-demand", villagerMinimumDemand); ++ if (PurpurConfig.version < 9) { ++ boolean oldValue = getBoolean("mobs.villager.lobotomize-1x1", villagerLobotomizeEnabled); ++ set("mobs.villager.lobotomize.enabled", oldValue); ++ set("mobs.villager.lobotomize-1x1", null); ++ } ++ if (PurpurConfig.version < 27) { ++ int oldValue = getInt("mobs.villager.lobotomize.check-interval", villagerLobotomizeCheckInterval); ++ set("mobs.villager.lobotomize.check-interval", oldValue == 60 ? 100 : oldValue); ++ } ++ villagerLobotomizeEnabled = getBoolean("mobs.villager.lobotomize.enabled", villagerLobotomizeEnabled); ++ villagerLobotomizeCheckInterval = getInt("mobs.villager.lobotomize.check-interval", villagerLobotomizeCheckInterval); ++ villagerDisplayTradeItem = getBoolean("mobs.villager.display-trade-item", villagerDisplayTradeItem); ++ villagerSpawnIronGolemRadius = getInt("mobs.villager.spawn-iron-golem.radius", villagerSpawnIronGolemRadius); ++ villagerSpawnIronGolemLimit = getInt("mobs.villager.spawn-iron-golem.limit", villagerSpawnIronGolemLimit); ++ } ++ ++ public boolean vindicatorRidable = false; ++ public boolean vindicatorRidableInWater = true; ++ public boolean vindicatorControllable = true; ++ public double vindicatorMaxHealth = 24.0D; ++ public double vindicatorJohnnySpawnChance = 0D; ++ public boolean vindicatorTakeDamageFromWater = false; ++ public boolean vindicatorAlwaysDropExp = false; ++ private void vindicatorSettings() { ++ vindicatorRidable = getBoolean("mobs.vindicator.ridable", vindicatorRidable); ++ vindicatorRidableInWater = getBoolean("mobs.vindicator.ridable-in-water", vindicatorRidableInWater); ++ vindicatorControllable = getBoolean("mobs.vindicator.controllable", vindicatorControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.vindicator.attributes.max-health", vindicatorMaxHealth); ++ set("mobs.vindicator.attributes.max-health", null); ++ set("mobs.vindicator.attributes.max_health", oldValue); ++ } ++ vindicatorMaxHealth = getDouble("mobs.vindicator.attributes.max_health", vindicatorMaxHealth); ++ vindicatorJohnnySpawnChance = getDouble("mobs.vindicator.johnny.spawn-chance", vindicatorJohnnySpawnChance); ++ vindicatorTakeDamageFromWater = getBoolean("mobs.vindicator.takes-damage-from-water", vindicatorTakeDamageFromWater); ++ vindicatorAlwaysDropExp = getBoolean("mobs.vindicator.always-drop-exp", vindicatorAlwaysDropExp); ++ } ++ ++ public boolean wanderingTraderRidable = false; ++ public boolean wanderingTraderRidableInWater = true; ++ public boolean wanderingTraderControllable = true; ++ public double wanderingTraderMaxHealth = 20.0D; ++ public boolean wanderingTraderFollowEmeraldBlock = false; ++ public boolean wanderingTraderCanBeLeashed = false; ++ public boolean wanderingTraderTakeDamageFromWater = false; ++ public boolean wanderingTraderAllowTrading = true; ++ public boolean wanderingTraderAlwaysDropExp = false; ++ private void wanderingTraderSettings() { ++ wanderingTraderRidable = getBoolean("mobs.wandering_trader.ridable", wanderingTraderRidable); ++ wanderingTraderRidableInWater = getBoolean("mobs.wandering_trader.ridable-in-water", wanderingTraderRidableInWater); ++ wanderingTraderControllable = getBoolean("mobs.wandering_trader.controllable", wanderingTraderControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wandering_trader.attributes.max-health", wanderingTraderMaxHealth); ++ set("mobs.wandering_trader.attributes.max-health", null); ++ set("mobs.wandering_trader.attributes.max_health", oldValue); ++ } ++ wanderingTraderMaxHealth = getDouble("mobs.wandering_trader.attributes.max_health", wanderingTraderMaxHealth); ++ wanderingTraderFollowEmeraldBlock = getBoolean("mobs.wandering_trader.follow-emerald-blocks", wanderingTraderFollowEmeraldBlock); ++ wanderingTraderCanBeLeashed = getBoolean("mobs.wandering_trader.can-be-leashed", wanderingTraderCanBeLeashed); ++ wanderingTraderTakeDamageFromWater = getBoolean("mobs.wandering_trader.takes-damage-from-water", wanderingTraderTakeDamageFromWater); ++ wanderingTraderAllowTrading = getBoolean("mobs.wandering_trader.allow-trading", wanderingTraderAllowTrading); ++ wanderingTraderAlwaysDropExp = getBoolean("mobs.wandering_trader.always-drop-exp", wanderingTraderAlwaysDropExp); ++ } ++ ++ public boolean wardenRidable = false; ++ public boolean wardenRidableInWater = true; ++ public boolean wardenControllable = true; ++ private void wardenSettings() { ++ wardenRidable = getBoolean("mobs.warden.ridable", wardenRidable); ++ wardenRidableInWater = getBoolean("mobs.warden.ridable-in-water", wardenRidableInWater); ++ wardenControllable = getBoolean("mobs.warden.controllable", wardenControllable); ++ } ++ ++ public boolean witchRidable = false; ++ public boolean witchRidableInWater = true; ++ public boolean witchControllable = true; ++ public double witchMaxHealth = 26.0D; ++ public boolean witchTakeDamageFromWater = false; ++ public boolean witchAlwaysDropExp = false; ++ private void witchSettings() { ++ witchRidable = getBoolean("mobs.witch.ridable", witchRidable); ++ witchRidableInWater = getBoolean("mobs.witch.ridable-in-water", witchRidableInWater); ++ witchControllable = getBoolean("mobs.witch.controllable", witchControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.witch.attributes.max-health", witchMaxHealth); ++ set("mobs.witch.attributes.max-health", null); ++ set("mobs.witch.attributes.max_health", oldValue); ++ } ++ witchMaxHealth = getDouble("mobs.witch.attributes.max_health", witchMaxHealth); ++ witchTakeDamageFromWater = getBoolean("mobs.witch.takes-damage-from-water", witchTakeDamageFromWater); ++ witchAlwaysDropExp = getBoolean("mobs.witch.always-drop-exp", witchAlwaysDropExp); ++ } ++ ++ public boolean witherRidable = false; ++ public boolean witherRidableInWater = true; ++ public boolean witherControllable = true; ++ public double witherMaxY = 320D; ++ public double witherMaxHealth = 300.0D; ++ public float witherHealthRegenAmount = 1.0f; ++ public int witherHealthRegenDelay = 20; ++ public boolean witherBypassMobGriefing = false; ++ public boolean witherTakeDamageFromWater = false; ++ public boolean witherCanRideVehicles = false; ++ public float witherExplosionRadius = 1.0F; ++ public boolean witherPlaySpawnSound = true; ++ public boolean witherAlwaysDropExp = false; ++ private void witherSettings() { ++ witherRidable = getBoolean("mobs.wither.ridable", witherRidable); ++ witherRidableInWater = getBoolean("mobs.wither.ridable-in-water", witherRidableInWater); ++ witherControllable = getBoolean("mobs.wither.controllable", witherControllable); ++ witherMaxY = getDouble("mobs.wither.ridable-max-y", witherMaxY); ++ if (PurpurConfig.version < 8) { ++ double oldValue = getDouble("mobs.wither.max-health", witherMaxHealth); ++ set("mobs.wither.max_health", null); ++ set("mobs.wither.attributes.max-health", oldValue); ++ } else if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wither.attributes.max-health", witherMaxHealth); ++ set("mobs.wither.attributes.max-health", null); ++ set("mobs.wither.attributes.max_health", oldValue); ++ } ++ witherMaxHealth = getDouble("mobs.wither.attributes.max_health", witherMaxHealth); ++ witherHealthRegenAmount = (float) getDouble("mobs.wither.health-regen-amount", witherHealthRegenAmount); ++ witherHealthRegenDelay = getInt("mobs.wither.health-regen-delay", witherHealthRegenDelay); ++ witherBypassMobGriefing = getBoolean("mobs.wither.bypass-mob-griefing", witherBypassMobGriefing); ++ witherTakeDamageFromWater = getBoolean("mobs.wither.takes-damage-from-water", witherTakeDamageFromWater); ++ witherCanRideVehicles = getBoolean("mobs.wither.can-ride-vehicles", witherCanRideVehicles); ++ witherExplosionRadius = (float) getDouble("mobs.wither.explosion-radius", witherExplosionRadius); ++ witherPlaySpawnSound = getBoolean("mobs.wither.play-spawn-sound", witherPlaySpawnSound); ++ witherAlwaysDropExp = getBoolean("mobs.wither.always-drop-exp", witherAlwaysDropExp); ++ } ++ ++ public boolean witherSkeletonRidable = false; ++ public boolean witherSkeletonRidableInWater = true; ++ public boolean witherSkeletonControllable = true; ++ public double witherSkeletonMaxHealth = 20.0D; ++ public boolean witherSkeletonTakeDamageFromWater = false; ++ public boolean witherSkeletonAlwaysDropExp = false; ++ private void witherSkeletonSettings() { ++ witherSkeletonRidable = getBoolean("mobs.wither_skeleton.ridable", witherSkeletonRidable); ++ witherSkeletonRidableInWater = getBoolean("mobs.wither_skeleton.ridable-in-water", witherSkeletonRidableInWater); ++ witherSkeletonControllable = getBoolean("mobs.wither_skeleton.controllable", witherSkeletonControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wither_skeleton.attributes.max-health", witherSkeletonMaxHealth); ++ set("mobs.wither_skeleton.attributes.max-health", null); ++ set("mobs.wither_skeleton.attributes.max_health", oldValue); ++ } ++ witherSkeletonMaxHealth = getDouble("mobs.wither_skeleton.attributes.max_health", witherSkeletonMaxHealth); ++ witherSkeletonTakeDamageFromWater = getBoolean("mobs.wither_skeleton.takes-damage-from-water", witherSkeletonTakeDamageFromWater); ++ witherSkeletonAlwaysDropExp = getBoolean("mobs.wither_skeleton.always-drop-exp", witherSkeletonAlwaysDropExp); ++ } ++ ++ public boolean wolfRidable = false; ++ public boolean wolfRidableInWater = true; ++ public boolean wolfControllable = true; ++ public double wolfMaxHealth = 8.0D; ++ public DyeColor wolfDefaultCollarColor = DyeColor.RED; ++ public boolean wolfMilkCuresRabies = true; ++ public double wolfNaturalRabid = 0.0D; ++ public int wolfBreedingTicks = 6000; ++ public boolean wolfTakeDamageFromWater = false; ++ public boolean wolfAlwaysDropExp = false; ++ private void wolfSettings() { ++ wolfRidable = getBoolean("mobs.wolf.ridable", wolfRidable); ++ wolfRidableInWater = getBoolean("mobs.wolf.ridable-in-water", wolfRidableInWater); ++ wolfControllable = getBoolean("mobs.wolf.controllable", wolfControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.wolf.attributes.max-health", wolfMaxHealth); ++ set("mobs.wolf.attributes.max-health", null); ++ set("mobs.wolf.attributes.max_health", oldValue); ++ } ++ wolfMaxHealth = getDouble("mobs.wolf.attributes.max_health", wolfMaxHealth); ++ try { ++ wolfDefaultCollarColor = DyeColor.valueOf(getString("mobs.wolf.default-collar-color", wolfDefaultCollarColor.name())); ++ } catch (IllegalArgumentException ignore) { ++ wolfDefaultCollarColor = DyeColor.RED; ++ } ++ wolfMilkCuresRabies = getBoolean("mobs.wolf.milk-cures-rabid-wolves", wolfMilkCuresRabies); ++ wolfNaturalRabid = getDouble("mobs.wolf.spawn-rabid-chance", wolfNaturalRabid); ++ wolfBreedingTicks = getInt("mobs.wolf.breeding-delay-ticks", wolfBreedingTicks); ++ wolfTakeDamageFromWater = getBoolean("mobs.wolf.takes-damage-from-water", wolfTakeDamageFromWater); ++ wolfAlwaysDropExp = getBoolean("mobs.wolf.always-drop-exp", wolfAlwaysDropExp); ++ } ++ ++ public boolean zoglinRidable = false; ++ public boolean zoglinRidableInWater = true; ++ public boolean zoglinControllable = true; ++ public double zoglinMaxHealth = 40.0D; ++ public boolean zoglinTakeDamageFromWater = false; ++ public boolean zoglinAlwaysDropExp = false; ++ private void zoglinSettings() { ++ zoglinRidable = getBoolean("mobs.zoglin.ridable", zoglinRidable); ++ zoglinRidableInWater = getBoolean("mobs.zoglin.ridable-in-water", zoglinRidableInWater); ++ zoglinControllable = getBoolean("mobs.zoglin.controllable", zoglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zoglin.attributes.max-health", zoglinMaxHealth); ++ set("mobs.zoglin.attributes.max-health", null); ++ set("mobs.zoglin.attributes.max_health", oldValue); ++ } ++ zoglinMaxHealth = getDouble("mobs.zoglin.attributes.max_health", zoglinMaxHealth); ++ zoglinTakeDamageFromWater = getBoolean("mobs.zoglin.takes-damage-from-water", zoglinTakeDamageFromWater); ++ zoglinAlwaysDropExp = getBoolean("mobs.zoglin.always-drop-exp", zoglinAlwaysDropExp); ++ } ++ ++ public boolean zombieRidable = false; ++ public boolean zombieRidableInWater = true; ++ public boolean zombieControllable = true; ++ public double zombieMaxHealth = 20.0D; ++ public double zombieSpawnReinforcements = 0.1D; ++ public boolean zombieJockeyOnlyBaby = true; ++ public double zombieJockeyChance = 0.05D; ++ public boolean zombieJockeyTryExistingChickens = true; ++ public boolean zombieAggressiveTowardsVillagerWhenLagging = true; ++ public boolean zombieBypassMobGriefing = false; ++ public boolean zombieTakeDamageFromWater = false; ++ public boolean zombieAlwaysDropExp = false; ++ public double zombieHeadVisibilityPercent = 0.5D; ++ private void zombieSettings() { ++ zombieRidable = getBoolean("mobs.zombie.ridable", zombieRidable); ++ zombieRidableInWater = getBoolean("mobs.zombie.ridable-in-water", zombieRidableInWater); ++ zombieControllable = getBoolean("mobs.zombie.controllable", zombieControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie.attributes.max-health", zombieMaxHealth); ++ set("mobs.zombie.attributes.max-health", null); ++ set("mobs.zombie.attributes.max_health", oldValue); ++ } ++ zombieMaxHealth = getDouble("mobs.zombie.attributes.max_health", zombieMaxHealth); ++ zombieSpawnReinforcements = getDouble("mobs.zombie.attributes.spawn_reinforcements", zombieSpawnReinforcements); ++ zombieJockeyOnlyBaby = getBoolean("mobs.zombie.jockey.only-babies", zombieJockeyOnlyBaby); ++ zombieJockeyChance = getDouble("mobs.zombie.jockey.chance", zombieJockeyChance); ++ zombieJockeyTryExistingChickens = getBoolean("mobs.zombie.jockey.try-existing-chickens", zombieJockeyTryExistingChickens); ++ zombieAggressiveTowardsVillagerWhenLagging = getBoolean("mobs.zombie.aggressive-towards-villager-when-lagging", zombieAggressiveTowardsVillagerWhenLagging); ++ zombieBypassMobGriefing = getBoolean("mobs.zombie.bypass-mob-griefing", zombieBypassMobGriefing); ++ zombieTakeDamageFromWater = getBoolean("mobs.zombie.takes-damage-from-water", zombieTakeDamageFromWater); ++ zombieAlwaysDropExp = getBoolean("mobs.zombie.always-drop-exp", zombieAlwaysDropExp); ++ zombieHeadVisibilityPercent = getDouble("mobs.zombie.head-visibility-percent", zombieHeadVisibilityPercent); ++ } ++ ++ public boolean zombieHorseRidableInWater = false; ++ public boolean zombieHorseCanSwim = false; ++ public double zombieHorseMaxHealthMin = 15.0D; ++ public double zombieHorseMaxHealthMax = 15.0D; ++ public double zombieHorseJumpStrengthMin = 0.4D; ++ public double zombieHorseJumpStrengthMax = 1.0D; ++ public double zombieHorseMovementSpeedMin = 0.2D; ++ public double zombieHorseMovementSpeedMax = 0.2D; ++ public double zombieHorseSpawnChance = 0.0D; ++ public boolean zombieHorseTakeDamageFromWater = false; ++ public boolean zombieHorseAlwaysDropExp = false; ++ private void zombieHorseSettings() { ++ zombieHorseRidableInWater = getBoolean("mobs.zombie_horse.ridable-in-water", zombieHorseRidableInWater); ++ zombieHorseCanSwim = getBoolean("mobs.zombie_horse.can-swim", zombieHorseCanSwim); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie_horse.attributes.max-health", zombieHorseMaxHealthMin); ++ set("mobs.zombie_horse.attributes.max-health", null); ++ set("mobs.zombie_horse.attributes.max_health.min", oldValue); ++ set("mobs.zombie_horse.attributes.max_health.max", oldValue); ++ } ++ zombieHorseMaxHealthMin = getDouble("mobs.zombie_horse.attributes.max_health.min", zombieHorseMaxHealthMin); ++ zombieHorseMaxHealthMax = getDouble("mobs.zombie_horse.attributes.max_health.max", zombieHorseMaxHealthMax); ++ zombieHorseJumpStrengthMin = getDouble("mobs.zombie_horse.attributes.jump_strength.min", zombieHorseJumpStrengthMin); ++ zombieHorseJumpStrengthMax = getDouble("mobs.zombie_horse.attributes.jump_strength.max", zombieHorseJumpStrengthMax); ++ zombieHorseMovementSpeedMin = getDouble("mobs.zombie_horse.attributes.movement_speed.min", zombieHorseMovementSpeedMin); ++ zombieHorseMovementSpeedMax = getDouble("mobs.zombie_horse.attributes.movement_speed.max", zombieHorseMovementSpeedMax); ++ zombieHorseSpawnChance = getDouble("mobs.zombie_horse.spawn-chance", zombieHorseSpawnChance); ++ zombieHorseTakeDamageFromWater = getBoolean("mobs.zombie_horse.takes-damage-from-water", zombieHorseTakeDamageFromWater); ++ zombieHorseAlwaysDropExp = getBoolean("mobs.zombie_horse.always-drop-exp", zombieHorseAlwaysDropExp); ++ } ++ ++ public boolean zombieVillagerRidable = false; ++ public boolean zombieVillagerRidableInWater = true; ++ public boolean zombieVillagerControllable = true; ++ public double zombieVillagerMaxHealth = 20.0D; ++ public double zombieVillagerSpawnReinforcements = 0.1D; ++ public boolean zombieVillagerJockeyOnlyBaby = true; ++ public double zombieVillagerJockeyChance = 0.05D; ++ public boolean zombieVillagerJockeyTryExistingChickens = true; ++ public boolean zombieVillagerTakeDamageFromWater = false; ++ public int zombieVillagerCuringTimeMin = 3600; ++ public int zombieVillagerCuringTimeMax = 6000; ++ public boolean zombieVillagerCureEnabled = true; ++ public boolean zombieVillagerAlwaysDropExp = false; ++ private void zombieVillagerSettings() { ++ zombieVillagerRidable = getBoolean("mobs.zombie_villager.ridable", zombieVillagerRidable); ++ zombieVillagerRidableInWater = getBoolean("mobs.zombie_villager.ridable-in-water", zombieVillagerRidableInWater); ++ zombieVillagerControllable = getBoolean("mobs.zombie_villager.controllable", zombieVillagerControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombie_villager.attributes.max-health", zombieVillagerMaxHealth); ++ set("mobs.zombie_villager.attributes.max-health", null); ++ set("mobs.zombie_villager.attributes.max_health", oldValue); ++ } ++ zombieVillagerMaxHealth = getDouble("mobs.zombie_villager.attributes.max_health", zombieVillagerMaxHealth); ++ zombieVillagerSpawnReinforcements = getDouble("mobs.zombie_villager.attributes.spawn_reinforcements", zombieVillagerSpawnReinforcements); ++ zombieVillagerJockeyOnlyBaby = getBoolean("mobs.zombie_villager.jockey.only-babies", zombieVillagerJockeyOnlyBaby); ++ zombieVillagerJockeyChance = getDouble("mobs.zombie_villager.jockey.chance", zombieVillagerJockeyChance); ++ zombieVillagerJockeyTryExistingChickens = getBoolean("mobs.zombie_villager.jockey.try-existing-chickens", zombieVillagerJockeyTryExistingChickens); ++ zombieVillagerTakeDamageFromWater = getBoolean("mobs.zombie_villager.takes-damage-from-water", zombieVillagerTakeDamageFromWater); ++ zombieVillagerCuringTimeMin = getInt("mobs.zombie_villager.curing_time.min", zombieVillagerCuringTimeMin); ++ zombieVillagerCuringTimeMax = getInt("mobs.zombie_villager.curing_time.max", zombieVillagerCuringTimeMax); ++ zombieVillagerCureEnabled = getBoolean("mobs.zombie_villager.cure.enabled", zombieVillagerCureEnabled); ++ zombieVillagerAlwaysDropExp = getBoolean("mobs.zombie_villager.always-drop-exp", zombieVillagerAlwaysDropExp); ++ } ++ ++ public boolean zombifiedPiglinRidable = false; ++ public boolean zombifiedPiglinRidableInWater = true; ++ public boolean zombifiedPiglinControllable = true; ++ public double zombifiedPiglinMaxHealth = 20.0D; ++ public double zombifiedPiglinSpawnReinforcements = 0.0D; ++ public boolean zombifiedPiglinJockeyOnlyBaby = true; ++ public double zombifiedPiglinJockeyChance = 0.05D; ++ public boolean zombifiedPiglinJockeyTryExistingChickens = true; ++ public boolean zombifiedPiglinCountAsPlayerKillWhenAngry = true; ++ public boolean zombifiedPiglinTakeDamageFromWater = false; ++ public boolean zombifiedPiglinAlwaysDropExp = false; ++ private void zombifiedPiglinSettings() { ++ zombifiedPiglinRidable = getBoolean("mobs.zombified_piglin.ridable", zombifiedPiglinRidable); ++ zombifiedPiglinRidableInWater = getBoolean("mobs.zombified_piglin.ridable-in-water", zombifiedPiglinRidableInWater); ++ zombifiedPiglinControllable = getBoolean("mobs.zombified_piglin.controllable", zombifiedPiglinControllable); ++ if (PurpurConfig.version < 10) { ++ double oldValue = getDouble("mobs.zombified_piglin.attributes.max-health", zombifiedPiglinMaxHealth); ++ set("mobs.zombified_piglin.attributes.max-health", null); ++ set("mobs.zombified_piglin.attributes.max_health", oldValue); ++ } ++ zombifiedPiglinMaxHealth = getDouble("mobs.zombified_piglin.attributes.max_health", zombifiedPiglinMaxHealth); ++ zombifiedPiglinSpawnReinforcements = getDouble("mobs.zombified_piglin.attributes.spawn_reinforcements", zombifiedPiglinSpawnReinforcements); ++ zombifiedPiglinJockeyOnlyBaby = getBoolean("mobs.zombified_piglin.jockey.only-babies", zombifiedPiglinJockeyOnlyBaby); ++ zombifiedPiglinJockeyChance = getDouble("mobs.zombified_piglin.jockey.chance", zombifiedPiglinJockeyChance); ++ zombifiedPiglinJockeyTryExistingChickens = getBoolean("mobs.zombified_piglin.jockey.try-existing-chickens", zombifiedPiglinJockeyTryExistingChickens); ++ zombifiedPiglinCountAsPlayerKillWhenAngry = getBoolean("mobs.zombified_piglin.count-as-player-kill-when-angry", zombifiedPiglinCountAsPlayerKillWhenAngry); ++ zombifiedPiglinTakeDamageFromWater = getBoolean("mobs.zombified_piglin.takes-damage-from-water", zombifiedPiglinTakeDamageFromWater); ++ zombifiedPiglinAlwaysDropExp = getBoolean("mobs.zombified_piglin.always-drop-exp", zombifiedPiglinAlwaysDropExp); ++ } ++ ++ public float hungerStarvationDamage = 1.0F; ++ private void hungerSettings() { ++ hungerStarvationDamage = (float) getDouble("hunger.starvation-damage", hungerStarvationDamage); ++ } ++ ++ public int conduitDistance = 16; ++ public double conduitDamageDistance = 8; ++ public float conduitDamageAmount = 4; ++ public Block[] conduitBlocks; ++ private void conduitSettings() { ++ conduitDistance = getInt("blocks.conduit.effect-distance", conduitDistance); ++ conduitDamageDistance = getDouble("blocks.conduit.mob-damage.distance", conduitDamageDistance); ++ conduitDamageAmount = (float) getDouble("blocks.conduit.mob-damage.damage-amount", conduitDamageAmount); ++ List conduitBlockList = new ArrayList<>(); ++ getList("blocks.conduit.valid-ring-blocks", new ArrayList(){{ ++ add("minecraft:prismarine"); ++ add("minecraft:prismarine_bricks"); ++ add("minecraft:sea_lantern"); ++ add("minecraft:dark_prismarine"); ++ }}).forEach(key -> { ++ Block block = BuiltInRegistries.BLOCK.get(new ResourceLocation(key.toString())); ++ if (!block.defaultBlockState().isAir()) { ++ conduitBlockList.add(block); ++ } ++ }); ++ conduitBlocks = conduitBlockList.toArray(Block[]::new); ++ } ++ ++ public float cauldronRainChance = 0.05F; ++ public float cauldronPowderSnowChance = 0.1F; ++ public float cauldronDripstoneWaterFillChance = 0.17578125F; ++ public float cauldronDripstoneLavaFillChance = 0.05859375F; ++ private void cauldronSettings() { ++ cauldronRainChance = (float) getDouble("blocks.cauldron.fill-chances.rain", cauldronRainChance); ++ cauldronPowderSnowChance = (float) getDouble("blocks.cauldron.fill-chances.powder-snow", cauldronPowderSnowChance); ++ cauldronDripstoneWaterFillChance = (float) getDouble("blocks.cauldron.fill-chances.dripstone-water", cauldronDripstoneWaterFillChance); ++ cauldronDripstoneLavaFillChance = (float) getDouble("blocks.cauldron.fill-chances.dripstone-lava", cauldronDripstoneLavaFillChance); ++ } ++} ++ +diff --git a/src/main/java/org/purpurmc/purpur/command/CompassCommand.java b/src/main/java/org/purpurmc/purpur/command/CompassCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..79b8490832d2a0cc7846ddcb091cb6bcac74ea45 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/CompassCommand.java +@@ -0,0 +1,27 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.task.CompassTask; ++ ++public class CompassCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("compass") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.compass")) ++ .executes(context -> { ++ ServerPlayer player = context.getSource().getPlayerOrException(); ++ CompassTask task = CompassTask.instance(); ++ if (player.compassBar()) { ++ task.removePlayer(player.getBukkitEntity()); ++ player.compassBar(false); ++ } else { ++ task.addPlayer(player.getBukkitEntity()); ++ player.compassBar(true); ++ } ++ return 1; ++ }) ++ ); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java b/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..40d2fab4a9728ac90c36e30c130f3116b7025d11 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/CreditsCommand.java +@@ -0,0 +1,35 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.network.protocol.game.ClientboundGameEventPacket; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class CreditsCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("credits") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.credits")) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.credits.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ ClientboundGameEventPacket packet = new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 1F); ++ player.connection.send(packet); ++ String output = String.format(PurpurConfig.creditsCommandOutput, player.getGameProfile().getName()); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/DemoCommand.java b/src/main/java/org/purpurmc/purpur/command/DemoCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..235f3cd89f675b70a6152a00534608c0902f19fd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/DemoCommand.java +@@ -0,0 +1,35 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.network.protocol.game.ClientboundGameEventPacket; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class DemoCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("demo") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.demo")) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.demo.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ ClientboundGameEventPacket packet = new ClientboundGameEventPacket(ClientboundGameEventPacket.DEMO_EVENT, 0); ++ player.connection.send(packet); ++ String output = String.format(PurpurConfig.demoCommandOutput, player.getGameProfile().getName()); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/PingCommand.java b/src/main/java/org/purpurmc/purpur/command/PingCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..179727c6b3171c040d1aaf069525f61a9a2d54d9 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/PingCommand.java +@@ -0,0 +1,33 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.craftbukkit.util.CraftChatMessage; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class PingCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("ping") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.ping")) ++ .executes((context) -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.ping.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ String output = String.format(PurpurConfig.pingCommandOutput, player.getGameProfile().getName(), player.latency); ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java b/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2621e54879e9ab0029a875f1d09eee67878b90d5 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/PurpurCommand.java +@@ -0,0 +1,66 @@ ++package org.purpurmc.purpur.command; ++ ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.ChatColor; ++import org.bukkit.Location; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++ ++import java.io.File; ++import java.util.Collections; ++import java.util.List; ++import java.util.stream.Collectors; ++import java.util.stream.Stream; ++ ++public class PurpurCommand extends Command { ++ public PurpurCommand(String name) { ++ super(name); ++ this.description = "Purpur related commands"; ++ this.usageMessage = "/purpur [reload | version]"; ++ this.setPermission("bukkit.command.purpur"); ++ } ++ ++ @Override ++ public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException { ++ if (args.length == 1) { ++ return Stream.of("reload", "version") ++ .filter(arg -> arg.startsWith(args[0].toLowerCase())) ++ .collect(Collectors.toList()); ++ } ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public boolean execute(CommandSender sender, String commandLabel, String[] args) { ++ if (!testPermission(sender)) return true; ++ ++ if (args.length != 1) { ++ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); ++ return false; ++ } ++ ++ if (args[0].equalsIgnoreCase("reload")) { ++ Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues."); ++ Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server."); ++ ++ MinecraftServer console = MinecraftServer.getServer(); ++ PurpurConfig.init((File) console.options.valueOf("purpur-settings")); ++ for (ServerLevel level : console.getAllLevels()) { ++ level.purpurConfig.init(); ++ level.resetBreedingCooldowns(); ++ } ++ console.server.reloadCount++; ++ ++ Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Purpur config reload complete."); ++ } else if (args[0].equalsIgnoreCase("version")) { ++ Command verCmd = org.bukkit.Bukkit.getServer().getCommandMap().getCommand("version"); ++ if (verCmd != null) { ++ return verCmd.execute(sender, commandLabel, new String[0]); ++ } ++ } ++ ++ return true; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java b/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2852c07adb080c34905f5d1b19efed8ea47eecc6 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/RamBarCommand.java +@@ -0,0 +1,44 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.RamBarTask; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class RamBarCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("rambar") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.rambar")) ++ .executes(context -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.rambar.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ boolean result = RamBarTask.instance().togglePlayer(player.getBukkitEntity()); ++ player.ramBar(result); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.rambarCommandOutput, ++ Placeholder.component("onoff", Component.translatable(result ? "options.on" : "options.off") ++ .color(result ? NamedTextColor.GREEN : NamedTextColor.RED)), ++ Placeholder.parsed("target", player.getGameProfile().getName())); ++ ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/RamCommand.java b/src/main/java/org/purpurmc/purpur/command/RamCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4ea0877f92b6733035d83a186c3d02c101c9b6cd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/RamCommand.java +@@ -0,0 +1,30 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.RamBarTask; ++ ++public class RamCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("ram") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.ram")) ++ .executes(context -> { ++ CommandSourceStack sender = context.getSource(); ++ RamBarTask ramBar = RamBarTask.instance(); ++ sender.sendSuccess(PaperAdventure.asVanilla(MiniMessage.miniMessage().deserialize(PurpurConfig.ramCommandOutput, ++ Placeholder.component("allocated", ramBar.format(ramBar.getAllocated())), ++ Placeholder.component("used", ramBar.format(ramBar.getUsed())), ++ Placeholder.component("xmx", ramBar.format(ramBar.getXmx())), ++ Placeholder.component("xms", ramBar.format(ramBar.getXms())), ++ Placeholder.unparsed("percent", ((int) (ramBar.getPercent() * 100)) + "%") ++ )), false); ++ return 1; ++ }) ++ ); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java b/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d8f9b044107ff7c29a83eb5378aa9f5465ba1995 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/TPSBarCommand.java +@@ -0,0 +1,44 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.EntityArgument; ++import net.minecraft.server.level.ServerPlayer; ++import org.purpurmc.purpur.PurpurConfig; ++import org.purpurmc.purpur.task.TPSBarTask; ++ ++import java.util.Collection; ++import java.util.Collections; ++ ++public class TPSBarCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("tpsbar") ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.tpsbar")) ++ .executes(context -> execute(context.getSource(), Collections.singleton(context.getSource().getPlayerOrException()))) ++ .then(Commands.argument("targets", EntityArgument.players()) ++ .requires(listener -> listener.hasPermission(2, "bukkit.command.tpsbar.other")) ++ .executes((context) -> execute(context.getSource(), EntityArgument.getPlayers(context, "targets"))) ++ ) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender, Collection targets) { ++ for (ServerPlayer player : targets) { ++ boolean result = TPSBarTask.instance().togglePlayer(player.getBukkitEntity()); ++ player.tpsBar(result); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.tpsbarCommandOutput, ++ Placeholder.component("onoff", Component.translatable(result ? "options.on" : "options.off") ++ .color(result ? NamedTextColor.GREEN : NamedTextColor.RED)), ++ Placeholder.parsed("target", player.getGameProfile().getName())); ++ ++ sender.sendSuccess(output, false); ++ } ++ return targets.size(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java b/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4bb475099bcf8f05d5f1474e7fbf29c57c2c40cd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/command/UptimeCommand.java +@@ -0,0 +1,55 @@ ++package org.purpurmc.purpur.command; ++ ++import com.mojang.brigadier.CommandDispatcher; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.server.MinecraftServer; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.util.concurrent.TimeUnit; ++import java.util.function.Function; ++ ++public class UptimeCommand { ++ public static void register(CommandDispatcher dispatcher) { ++ dispatcher.register(Commands.literal("uptime") ++ .requires((listener) -> listener.hasPermission(2, "bukkit.command.uptime")) ++ .executes((context) -> execute(context.getSource())) ++ ); ++ } ++ ++ private static int execute(CommandSourceStack sender) { ++ Data data = new Data(); ++ ++ data.format = PurpurConfig.uptimeFormat; ++ data.hide = true; ++ data.millis = System.currentTimeMillis() - MinecraftServer.startTimeMillis; ++ ++ process(data, "", PurpurConfig.uptimeDay, PurpurConfig.uptimeDays, TimeUnit.DAYS, TimeUnit.MILLISECONDS::toDays); ++ process(data, "", PurpurConfig.uptimeHour, PurpurConfig.uptimeHours, TimeUnit.HOURS, TimeUnit.MILLISECONDS::toHours); ++ process(data, "", PurpurConfig.uptimeMinute, PurpurConfig.uptimeMinutes, TimeUnit.MINUTES, TimeUnit.MILLISECONDS::toMinutes); ++ data.hide = false; // never hide seconds ++ process(data, "", PurpurConfig.uptimeSecond, PurpurConfig.uptimeSeconds, TimeUnit.SECONDS, TimeUnit.MILLISECONDS::toSeconds); ++ ++ Component output = MiniMessage.miniMessage().deserialize(PurpurConfig.uptimeCommandOutput, Placeholder.unparsed("uptime", data.format)); ++ sender.sendSuccess(output, false); ++ return 1; ++ } ++ ++ private static void process(Data data, String replace, String singular, String plural, TimeUnit unit, Function func) { ++ if (data.format.contains(replace)) { ++ long val = func.apply(data.millis); ++ if (data.hide) data.hide = val == 0; ++ if (!data.hide) data.millis -= unit.toMillis(val); ++ data.format = data.format.replace(replace, data.hide ? "" : String.format(val == 1 ? singular : plural, val)); ++ } ++ } ++ ++ private static class Data { ++ String format; ++ boolean hide; ++ long millis; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ed494e0ad278813a0eb261101447b84cca3ad7aa +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/FlyingMoveControllerWASD.java +@@ -0,0 +1,71 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++ ++public class FlyingMoveControllerWASD extends MoveControllerWASD { ++ protected final float groundSpeedModifier; ++ protected final float flyingSpeedModifier; ++ protected int tooHighCooldown = 0; ++ protected boolean setNoGravityFlag; ++ ++ public FlyingMoveControllerWASD(Mob entity) { ++ this(entity, 1.0F); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier) { ++ this(entity, groundSpeedModifier, 1.0F, true); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier, float flyingSpeedModifier) { ++ this(entity, groundSpeedModifier, flyingSpeedModifier, true); ++ } ++ ++ public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier, float flyingSpeedModifier, boolean setNoGravityFlag) { ++ super(entity); ++ this.groundSpeedModifier = groundSpeedModifier; ++ this.flyingSpeedModifier = flyingSpeedModifier; ++ this.setNoGravityFlag = setNoGravityFlag; ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = Math.max(0.0F, rider.getForwardMot()); ++ float vertical = forward == 0.0F ? 0.0F : -(rider.xRotO / 45.0F); ++ float strafe = rider.getStrafeMot(); ++ ++ if (rider.jumping && spacebarEvent(entity)) { ++ entity.onSpacebar(); ++ } ++ ++ if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) { ++ if (tooHighCooldown <= 0) { ++ tooHighCooldown = 20; ++ } ++ entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.05D, 0.0D)); ++ vertical = 0.0F; ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED)); ++ float speed = (float) getSpeedModifier(); ++ ++ if (entity.onGround) { ++ speed *= groundSpeedModifier; // TODO = fix this! ++ } else { ++ speed *= flyingSpeedModifier; ++ } ++ ++ if (setNoGravityFlag) { ++ entity.setNoGravity(forward > 0); ++ } ++ ++ entity.setSpeed(speed); ++ entity.setVerticalMot(vertical); ++ entity.setStrafeMot(strafe); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9383c07fa53141127106a1f289366a040960d52e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java +@@ -0,0 +1,63 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.phys.Vec3; ++ ++public class FlyingWithSpacebarMoveControllerWASD extends FlyingMoveControllerWASD { ++ public FlyingWithSpacebarMoveControllerWASD(Mob entity) { ++ super(entity); ++ } ++ ++ public FlyingWithSpacebarMoveControllerWASD(Mob entity, float groundSpeedModifier) { ++ super(entity, groundSpeedModifier); ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot() * 0.5F; ++ float vertical = 0; ++ ++ if (forward < 0.0F) { ++ forward *= 0.5F; ++ strafe *= 0.5F; ++ } ++ ++ float speed = (float) entity.getAttributeValue(Attributes.MOVEMENT_SPEED); ++ ++ if (entity.onGround) { ++ speed *= groundSpeedModifier; ++ } ++ ++ if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar()) { ++ entity.setNoGravity(true); ++ vertical = 1.0F; ++ } else { ++ entity.setNoGravity(false); ++ } ++ ++ if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) { ++ if (tooHighCooldown <= 0) { ++ tooHighCooldown = 20; ++ } ++ entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.2D, 0.0D)); ++ vertical = 0.0F; ++ } ++ ++ setSpeedModifier(speed); ++ entity.setSpeed((float) getSpeedModifier()); ++ entity.setVerticalMot(vertical); ++ entity.setStrafeMot(strafe); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ ++ Vec3 mot = entity.getDeltaMovement(); ++ if (mot.y > 0.2D) { ++ entity.setDeltaMovement(mot.x, 0.2D, mot.z); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b8c25c96e95dd5ec3ad9fa4c41bd6c08e144832d +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/LookControllerWASD.java +@@ -0,0 +1,76 @@ ++package org.purpurmc.purpur.controller; ++ ++ ++import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.control.LookControl; ++import net.minecraft.world.entity.player.Player; ++ ++public class LookControllerWASD extends LookControl { ++ protected final Mob entity; ++ private float yOffset = 0; ++ private float xOffset = 0; ++ ++ public LookControllerWASD(Mob entity) { ++ super(entity); ++ this.entity = entity; ++ } ++ ++ // tick ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ ++ protected void purpurTick(Player rider) { ++ setYawPitch(rider.getYRot(), rider.getXRot()); ++ } ++ ++ public void vanillaTick() { ++ super.tick(); ++ } ++ ++ public void setYawPitch(float yRot, float xRot) { ++ entity.setXRot(normalizePitch(xRot + xOffset)); ++ entity.setYRot(normalizeYaw(yRot + yOffset)); ++ entity.setYHeadRot(entity.getYRot()); ++ entity.xRotO = entity.getXRot(); ++ entity.yRotO = entity.getYRot(); ++ ++ entity.tracker.broadcast(new ClientboundMoveEntityPacket ++ .PosRot(entity.getId(), ++ (short) 0, (short) 0, (short) 0, ++ (byte) Mth.floor(entity.getYRot() * 256.0F / 360.0F), ++ (byte) Mth.floor(entity.getXRot() * 256.0F / 360.0F), ++ entity.onGround)); ++ } ++ ++ public void setOffsets(float yaw, float pitch) { ++ yOffset = yaw; ++ xOffset = pitch; ++ } ++ ++ public float normalizeYaw(float yaw) { ++ yaw %= 360.0f; ++ if (yaw >= 180.0f) { ++ yaw -= 360.0f; ++ } else if (yaw < -180.0f) { ++ yaw += 360.0f; ++ } ++ return yaw; ++ } ++ ++ public float normalizePitch(float pitch) { ++ if (pitch > 90.0f) { ++ pitch = 90.0f; ++ } else if (pitch < -90.0f) { ++ pitch = -90.0f; ++ } ++ return pitch; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..21fd6ea2a482758a3016e3bc2cdebe2d89267481 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/MoveControllerWASD.java +@@ -0,0 +1,89 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.ai.control.MoveControl; ++import net.minecraft.world.entity.player.Player; ++import org.purpurmc.purpur.event.entity.RidableSpacebarEvent; ++ ++public class MoveControllerWASD extends MoveControl { ++ protected final Mob entity; ++ private final double speedModifier; ++ ++ public MoveControllerWASD(Mob entity) { ++ this(entity, 1.0D); ++ } ++ ++ public MoveControllerWASD(Mob entity, double speedModifier) { ++ super(entity); ++ this.entity = entity; ++ this.speedModifier = speedModifier; ++ } ++ ++ @Override ++ public boolean hasWanted() { ++ return entity.getRider() != null ? strafeForwards != 0 || strafeRight != 0 : super.hasWanted(); ++ } ++ ++ @Override ++ public void tick() { ++ if (entity.getRider() != null && entity.isControllable()) { ++ purpurTick(entity.getRider()); ++ } else { ++ vanillaTick(); ++ } ++ } ++ ++ public void vanillaTick() { ++ super.tick(); ++ } ++ ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot() * 0.5F; ++ float strafe = rider.getStrafeMot() * 0.25F; ++ ++ if (forward <= 0.0F) { ++ forward *= 0.5F; ++ } ++ ++ float yawOffset = 0; ++ if (strafe != 0) { ++ if (forward == 0) { ++ yawOffset += strafe > 0 ? -90 : 90; ++ forward = Math.abs(strafe * 2); ++ } else { ++ yawOffset += strafe > 0 ? -30 : 30; ++ strafe /= 2; ++ if (forward < 0) { ++ yawOffset += strafe > 0 ? -110 : 110; ++ forward *= -1; ++ } ++ } ++ } else if (forward < 0) { ++ yawOffset -= 180; ++ forward *= -1; ++ } ++ ++ ((LookControllerWASD) entity.getLookControl()).setOffsets(yawOffset, 0); ++ ++ if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar() && entity.onGround) { ++ entity.jumpFromGround(); ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier); ++ ++ entity.setSpeed((float) getSpeedModifier()); ++ entity.setForwardMot(forward); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++ ++ public static boolean spacebarEvent(Mob entity) { ++ if (RidableSpacebarEvent.getHandlerList().getRegisteredListeners().length > 0) { ++ return new RidableSpacebarEvent(entity.getBukkitEntity()).callEvent(); ++ } else { ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java b/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ba2a37dad43e238e54632975abea8ee6fafaa9e0 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/controller/WaterMoveControllerWASD.java +@@ -0,0 +1,50 @@ ++package org.purpurmc.purpur.controller; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.player.Player; ++ ++public class WaterMoveControllerWASD extends MoveControllerWASD { ++ private final double speedModifier; ++ ++ public WaterMoveControllerWASD(Mob entity) { ++ this(entity, 1.0D); ++ } ++ ++ public WaterMoveControllerWASD(Mob entity, double speedModifier) { ++ super(entity); ++ this.speedModifier = speedModifier; ++ } ++ ++ @Override ++ public void purpurTick(Player rider) { ++ float forward = rider.getForwardMot(); ++ float strafe = rider.getStrafeMot() * 0.5F; // strafe slower by default ++ float vertical = -(rider.xRotO / 90); ++ ++ if (forward == 0.0F) { ++ // strafe slower if not moving forward ++ strafe *= 0.5F; ++ // do not move vertically if not moving forward ++ vertical = 0.0F; ++ } else if (forward < 0.0F) { ++ // water animals can't swim backwards ++ forward = 0.0F; ++ vertical = 0.0F; ++ } ++ ++ if (rider.jumping && spacebarEvent(entity)) { ++ entity.onSpacebar(); ++ } ++ ++ setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier); ++ entity.setSpeed((float) getSpeedModifier() * 0.1F); ++ ++ entity.setForwardMot(forward * (float) speedModifier); ++ entity.setStrafeMot(strafe * (float) speedModifier); ++ entity.setVerticalMot(vertical * (float) speedModifier); ++ ++ setForward(entity.getForwardMot()); ++ setStrafe(entity.getStrafeMot()); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java b/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f0279d6cdc93f524f321c3c40967fdeeb8d2c46b +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java +@@ -0,0 +1,104 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.core.particles.ParticleTypes; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.entity.animal.Dolphin; ++import net.minecraft.world.entity.projectile.LlamaSpit; ++import net.minecraft.world.entity.projectile.ProjectileUtil; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.EntityHitResult; ++import net.minecraft.world.phys.HitResult; ++import net.minecraft.world.phys.Vec3; ++ ++public class DolphinSpit extends LlamaSpit { ++ public LivingEntity dolphin; ++ public int ticksLived; ++ ++ public DolphinSpit(EntityType type, Level world) { ++ super(type, world); ++ } ++ ++ public DolphinSpit(Level world, Dolphin dolphin) { ++ this(EntityType.LLAMA_SPIT, world); ++ setOwner(dolphin.getRider() != null ? dolphin.getRider() : dolphin); ++ this.dolphin = dolphin; ++ this.setPos( ++ dolphin.getX() - (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(dolphin.yBodyRot * 0.017453292F), ++ dolphin.getEyeY() - 0.10000000149011612D, ++ dolphin.getZ() + (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(dolphin.yBodyRot * 0.017453292F)); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResult(this, this::canHitEntity); ++ ++ this.preOnHit(hitResult); ++ ++ double x = this.getX() + mot.x; ++ double y = this.getY() + mot.y; ++ double z = this.getZ() + mot.z; ++ ++ this.updateRotation(); ++ ++ Vec3 motDouble = mot.scale(2.0); ++ for (int i = 0; i < 5; i++) { ++ ((ServerLevel) level).sendParticles(null, ParticleTypes.BUBBLE, ++ getX() + random.nextFloat() / 2 - 0.25F, ++ getY() + random.nextFloat() / 2 - 0.25F, ++ getZ() + random.nextFloat() / 2 - 0.25F, ++ 0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true); ++ } ++ ++ if (++ticksLived > 20) { ++ this.discard(); ++ } else { ++ this.setDeltaMovement(mot.scale(0.99D)); ++ if (!this.isNoGravity()) { ++ this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D)); ++ } ++ ++ this.setPos(x, y, z); ++ } ++ } ++ ++ @Override ++ public void shoot(double x, double y, double z, float speed, float inaccuracy) { ++ setDeltaMovement(new Vec3(x, y, z).normalize().add( ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy) ++ .scale(speed)); ++ } ++ ++ @Override ++ protected void onHitEntity(EntityHitResult entityHitResult) { ++ Entity shooter = this.getOwner(); ++ if (shooter instanceof LivingEntity) { ++ entityHitResult.getEntity().hurt(entityHitResult.getEntity().damageSources().mobProjectile(this, (LivingEntity) shooter), level.purpurConfig.dolphinSpitDamage); ++ } ++ } ++ ++ @Override ++ protected void onHitBlock(BlockHitResult blockHitResult) { ++ if (this.hitCancelled) { ++ return; ++ } ++ BlockState state = this.level.getBlockState(blockHitResult.getBlockPos()); ++ state.onProjectileHit(this.level, state, blockHitResult, this); ++ this.discard(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/GlowSquidColor.java b/src/main/java/org/purpurmc/purpur/entity/GlowSquidColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c90256f4c16ffdb2d8e767e837ea36ac7a6613be +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/GlowSquidColor.java +@@ -0,0 +1,61 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.util.RandomSource; ++ ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++ ++public enum GlowSquidColor { ++ BLUE, RED, GREEN, PINK, YELLOW, ORANGE, INDIGO, PURPLE, WHITE, GRAY, BLACK; ++ ++ @Override ++ public String toString() { ++ return this.name().toLowerCase(Locale.ROOT); ++ } ++ ++ public enum Mode { ++ RAINBOW(RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PURPLE), ++ ALL_COLORS(BLUE, RED, GREEN, PINK, YELLOW, ORANGE, INDIGO, PURPLE, WHITE, GRAY, BLACK), ++ TRANS_PRIDE(BLUE, WHITE, PINK), ++ LESBIAN_PRIDE(RED, ORANGE, WHITE, PINK, PURPLE), ++ BI_PRIDE(BLUE, PINK, PURPLE), ++ GAY_PRIDE(BLUE, GREEN, WHITE), ++ PAN_PRIDE(PINK, YELLOW, BLUE), ++ ACE_PRIDE(BLACK, GRAY, WHITE, PURPLE), ++ ARO_PRIDE(BLACK, GRAY, WHITE, GREEN), ++ ENBY_PRIDE(YELLOW, WHITE, BLACK, PURPLE), ++ GENDERFLUID(PURPLE, WHITE, BLACK, PINK, BLUE), ++ MONOCHROME(BLACK, GRAY, WHITE), ++ VANILLA(BLUE); ++ ++ private static final Map BY_NAME = new HashMap<>(); ++ ++ static { ++ Arrays.stream(values()).forEach(mode -> BY_NAME.put(mode.name(), mode)); ++ } ++ ++ private final List colors = new ArrayList<>(); ++ ++ Mode(GlowSquidColor... colors) { ++ this.colors.addAll(Arrays.stream(colors).toList()); ++ } ++ ++ public static Mode get(String string) { ++ Mode mode = BY_NAME.get(string.toUpperCase(Locale.ROOT)); ++ return mode == null ? RAINBOW : mode; ++ } ++ ++ public GlowSquidColor getRandom(RandomSource random) { ++ return this.colors.get(random.nextInt(this.colors.size())); ++ } ++ ++ @Override ++ public String toString() { ++ return this.name().toLowerCase(Locale.ROOT); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java b/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1542f038621b97a298a0fb31ab3be912e2bcd0d6 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java +@@ -0,0 +1,119 @@ ++package org.purpurmc.purpur.entity; ++ ++import net.minecraft.core.particles.ParticleTypes; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.entity.decoration.ArmorStand; ++import net.minecraft.world.entity.monster.Phantom; ++import net.minecraft.world.entity.projectile.LlamaSpit; ++import net.minecraft.world.entity.projectile.ProjectileUtil; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockBehaviour; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.EntityHitResult; ++import net.minecraft.world.phys.HitResult; ++import net.minecraft.world.phys.Vec3; ++ ++public class PhantomFlames extends LlamaSpit { ++ public Phantom phantom; ++ public int ticksLived; ++ public boolean canGrief = false; ++ ++ public PhantomFlames(EntityType type, Level world) { ++ super(type, world); ++ } ++ ++ public PhantomFlames(Level world, Phantom phantom) { ++ this(EntityType.LLAMA_SPIT, world); ++ setOwner(phantom.getRider() != null ? phantom.getRider() : phantom); ++ this.phantom = phantom; ++ this.setPos( ++ phantom.getX() - (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(phantom.yBodyRot * 0.017453292F), ++ phantom.getEyeY() - 0.10000000149011612D, ++ phantom.getZ() + (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(phantom.yBodyRot * 0.017453292F)); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResult(this, this::canHitEntity); ++ ++ this.preOnHit(hitResult); ++ ++ double x = this.getX() + mot.x; ++ double y = this.getY() + mot.y; ++ double z = this.getZ() + mot.z; ++ ++ this.updateRotation(); ++ ++ Vec3 motDouble = mot.scale(2.0); ++ for (int i = 0; i < 5; i++) { ++ ((ServerLevel) level).sendParticles(null, ParticleTypes.FLAME, ++ getX() + random.nextFloat() / 2 - 0.25F, ++ getY() + random.nextFloat() / 2 - 0.25F, ++ getZ() + random.nextFloat() / 2 - 0.25F, ++ 0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true); ++ } ++ ++ if (++ticksLived > 20) { ++ this.discard(); ++ } else if (this.level.getBlockStates(this.getBoundingBox()).noneMatch(BlockBehaviour.BlockStateBase::isAir)) { ++ this.discard(); ++ } else if (this.isInWaterOrBubble()) { ++ this.discard(); ++ } else { ++ this.setDeltaMovement(mot.scale(0.99D)); ++ if (!this.isNoGravity()) { ++ this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D)); ++ } ++ ++ this.setPos(x, y, z); ++ } ++ } ++ ++ @Override ++ public void shoot(double x, double y, double z, float speed, float inaccuracy) { ++ setDeltaMovement(new Vec3(x, y, z).normalize().add( ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy, ++ random.nextGaussian() * (double) 0.0075F * (double) inaccuracy) ++ .scale(speed)); ++ } ++ ++ @Override ++ protected void onHitEntity(EntityHitResult entityHitResult) { ++ Entity shooter = this.getOwner(); ++ if (shooter instanceof LivingEntity) { ++ Entity target = entityHitResult.getEntity(); ++ if (canGrief || (target instanceof LivingEntity && !(target instanceof ArmorStand))) { ++ boolean hurt = target.hurt(target.damageSources().mobProjectile(this, (LivingEntity) shooter), level.purpurConfig.phantomFlameDamage); ++ if (hurt && level.purpurConfig.phantomFlameFireTime > 0) { ++ target.setSecondsOnFire(level.purpurConfig.phantomFlameFireTime); ++ } ++ } ++ } ++ } ++ ++ @Override ++ protected void onHitBlock(BlockHitResult blockHitResult) { ++ if (this.hitCancelled) { ++ return; ++ } ++ if (this.canGrief) { ++ BlockState state = this.level.getBlockState(blockHitResult.getBlockPos()); ++ state.onProjectileHit(this.level, state, blockHitResult, this); ++ } ++ this.discard(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8babdaddd8b33278aea0369dbbeeb445abe45016 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/HasRider.java +@@ -0,0 +1,20 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.goal.Goal; ++ ++import java.util.EnumSet; ++ ++public class HasRider extends Goal { ++ public final Mob entity; ++ ++ public HasRider(Mob entity) { ++ this.entity = entity; ++ setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK, Flag.TARGET, Flag.UNKNOWN_BEHAVIOR)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ return entity.getRider() != null && entity.isControllable(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..432f4f3d82af2f19820890b68d33189a9f2c69f9 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/HorseHasRider.java +@@ -0,0 +1,17 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.animal.horse.AbstractHorse; ++ ++public class HorseHasRider extends HasRider { ++ public final AbstractHorse horse; ++ ++ public HorseHasRider(AbstractHorse entity) { ++ super(entity); ++ this.horse = entity; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && horse.isSaddled(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java b/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..18a95e043cbffa65eeaaf65ff7695e5dc939820c +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/LlamaHasRider.java +@@ -0,0 +1,17 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.world.entity.animal.horse.Llama; ++ ++public class LlamaHasRider extends HasRider { ++ public final Llama llama; ++ ++ public LlamaHasRider(Llama entity) { ++ super(entity); ++ this.llama = entity; ++ } ++ ++ @Override ++ public boolean canUse() { ++ return super.canUse() && llama.isSaddled() && llama.isControllable(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java b/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java +new file mode 100644 +index 0000000000000000000000000000000000000000..115a3b36cbb7716b28ef940a29ca97ac42a8a521 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/ai/ReceiveFlower.java +@@ -0,0 +1,91 @@ ++package org.purpurmc.purpur.entity.ai; ++ ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.ai.goal.Goal; ++import net.minecraft.world.entity.animal.IronGolem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.block.Blocks; ++ ++import java.util.EnumSet; ++import java.util.UUID; ++ ++public class ReceiveFlower extends Goal { ++ private final IronGolem irongolem; ++ private ServerPlayer target; ++ private int cooldown; ++ ++ public ReceiveFlower(IronGolem entity) { ++ this.irongolem = entity; ++ setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK)); ++ } ++ ++ @Override ++ public boolean canUse() { ++ if (this.irongolem.getOfferFlowerTick() > 0) { ++ return false; ++ } ++ if (!this.irongolem.isAngry()) { ++ return false; ++ } ++ UUID uuid = this.irongolem.getPersistentAngerTarget(); ++ if (uuid == null) { ++ return false; ++ } ++ Entity target = ((ServerLevel) this.irongolem.level).getEntity(uuid); ++ if (!(target instanceof ServerPlayer player)) { ++ return false; ++ } ++ InteractionHand hand = getPoppyHand(player); ++ if (hand == null) { ++ return false; ++ } ++ removeFlower(player, hand); ++ this.target = player; ++ return true; ++ } ++ ++ @Override ++ public boolean canContinueToUse() { ++ return this.cooldown > 0; ++ } ++ ++ @Override ++ public void start() { ++ this.cooldown = 100; ++ this.irongolem.stopBeingAngry(); ++ this.irongolem.offerFlower(true); ++ } ++ ++ @Override ++ public void stop() { ++ this.irongolem.offerFlower(false); ++ this.target = null; ++ } ++ ++ @Override ++ public void tick() { ++ this.irongolem.getLookControl().setLookAt(this.target, 30.0F, 30.0F); ++ --this.cooldown; ++ } ++ ++ private InteractionHand getPoppyHand(ServerPlayer player) { ++ if (isPoppy(player.getMainHandItem())) { ++ return InteractionHand.MAIN_HAND; ++ } ++ if (isPoppy(player.getOffhandItem())) { ++ return InteractionHand.OFF_HAND; ++ } ++ return null; ++ } ++ ++ private void removeFlower(ServerPlayer player, InteractionHand hand) { ++ player.setItemInHand(hand, ItemStack.EMPTY); ++ } ++ ++ private boolean isPoppy(ItemStack item) { ++ return item.getItem() == Blocks.POPPY.asItem(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/gui/GUIColor.java b/src/main/java/org/purpurmc/purpur/gui/GUIColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0f2e7e0b81620c8581949bd5f0bdb567cd93c17e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/gui/GUIColor.java +@@ -0,0 +1,54 @@ ++package org.purpurmc.purpur.gui; ++ ++import net.md_5.bungee.api.ChatColor; ++ ++import java.awt.Color; ++import java.util.HashMap; ++import java.util.Map; ++ ++public enum GUIColor { ++ BLACK(ChatColor.BLACK, new Color(0x000000)), ++ DARK_BLUE(ChatColor.DARK_BLUE, new Color(0x0000AA)), ++ DARK_GREEN(ChatColor.DARK_GREEN, new Color(0x00AA00)), ++ DARK_AQUA(ChatColor.DARK_AQUA, new Color(0x009999)), ++ DARK_RED(ChatColor.DARK_RED, new Color(0xAA0000)), ++ DARK_PURPLE(ChatColor.DARK_PURPLE, new Color(0xAA00AA)), ++ GOLD(ChatColor.GOLD, new Color(0xBB8800)), ++ GRAY(ChatColor.GRAY, new Color(0x888888)), ++ DARK_GRAY(ChatColor.DARK_GRAY, new Color(0x444444)), ++ BLUE(ChatColor.BLUE, new Color(0x5555FF)), ++ GREEN(ChatColor.GREEN, new Color(0x55FF55)), ++ AQUA(ChatColor.AQUA, new Color(0x55DDDD)), ++ RED(ChatColor.RED, new Color(0xFF5555)), ++ LIGHT_PURPLE(ChatColor.LIGHT_PURPLE, new Color(0xFF55FF)), ++ YELLOW(ChatColor.YELLOW, new Color(0xFFBB00)), ++ WHITE(ChatColor.WHITE, new Color(0xBBBBBB)); ++ ++ private final ChatColor chat; ++ private final Color color; ++ ++ private static final Map BY_CHAT = new HashMap<>(); ++ ++ GUIColor(ChatColor chat, Color color) { ++ this.chat = chat; ++ this.color = color; ++ } ++ ++ public Color getColor() { ++ return color; ++ } ++ ++ public String getCode() { ++ return chat.toString(); ++ } ++ ++ public static GUIColor getColor(ChatColor chat) { ++ return BY_CHAT.get(chat); ++ } ++ ++ static { ++ for (GUIColor color : values()) { ++ BY_CHAT.put(color.chat, color); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/gui/JColorTextPane.java b/src/main/java/org/purpurmc/purpur/gui/JColorTextPane.java +new file mode 100644 +index 0000000000000000000000000000000000000000..33e89b4c00fa8318506b36cbe49fe4e412e0a9a1 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/gui/JColorTextPane.java +@@ -0,0 +1,78 @@ ++package org.purpurmc.purpur.gui; ++ ++import com.google.common.collect.Sets; ++import net.md_5.bungee.api.ChatColor; ++import net.md_5.bungee.api.chat.BaseComponent; ++import net.md_5.bungee.api.chat.TextComponent; ++ ++import javax.swing.JTextPane; ++import javax.swing.Timer; ++import javax.swing.text.AttributeSet; ++import javax.swing.text.BadLocationException; ++import javax.swing.text.SimpleAttributeSet; ++import javax.swing.text.StyleConstants; ++import javax.swing.text.StyleContext; ++import java.util.Set; ++ ++public class JColorTextPane extends JTextPane { ++ private static final GUIColor DEFAULT_COLOR = GUIColor.BLACK; ++ ++ public void append(String msg) { ++ // TODO: update to use adventure instead ++ BaseComponent[] components = TextComponent.fromLegacyText(DEFAULT_COLOR.getCode() + msg, ChatColor.BLACK); ++ for (BaseComponent component : components) { ++ String text = component.toPlainText(); ++ if (text == null || text.isEmpty()) { ++ continue; ++ } ++ ++ GUIColor guiColor = GUIColor.getColor(component.getColor()); ++ ++ StyleContext context = StyleContext.getDefaultStyleContext(); ++ AttributeSet attr = context.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, guiColor.getColor()); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Bold, component.isBold() || guiColor != DEFAULT_COLOR); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Italic, component.isItalic()); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Underline, component.isUnderlined()); ++ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.StrikeThrough, component.isStrikethrough()); ++ //attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Blink, component.isObfuscated()); // no such thing as Blink, sadly ++ ++ try { ++ int pos = getDocument().getLength(); ++ getDocument().insertString(pos, text, attr); ++ ++ if (component.isObfuscated()) { ++ // dirty hack to blink some text ++ Blink blink = new Blink(pos, text.length(), attr, context.addAttribute(attr, StyleConstants.Foreground, getBackground())); ++ BLINKS.add(blink); ++ } ++ } catch (BadLocationException ignore) { ++ } ++ } ++ } ++ ++ private static final Set BLINKS = Sets.newHashSet(); ++ private static boolean SYNC_BLINK; ++ ++ static { ++ new Timer(500, e -> { ++ SYNC_BLINK = !SYNC_BLINK; ++ BLINKS.forEach(Blink::blink); ++ }).start(); ++ } ++ ++ public class Blink { ++ private final int start, length; ++ private final AttributeSet attr1, attr2; ++ ++ private Blink(int start, int length, AttributeSet attr1, AttributeSet attr2) { ++ this.start = start; ++ this.length = length; ++ this.attr1 = attr1; ++ this.attr2 = attr2; ++ } ++ ++ private void blink() { ++ getStyledDocument().setCharacterAttributes(start, length, SYNC_BLINK ? attr1 : attr2, true); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/gui/util/HighlightErrorConverter.java b/src/main/java/org/purpurmc/purpur/gui/util/HighlightErrorConverter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..15a226e3854d731f7724025ea3459c8ace07630c +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/gui/util/HighlightErrorConverter.java +@@ -0,0 +1,85 @@ ++package org.purpurmc.purpur.gui.util; ++ ++import org.apache.logging.log4j.Level; ++import org.apache.logging.log4j.core.LogEvent; ++import org.apache.logging.log4j.core.config.Configuration; ++import org.apache.logging.log4j.core.config.plugins.Plugin; ++import org.apache.logging.log4j.core.layout.PatternLayout; ++import org.apache.logging.log4j.core.pattern.ConverterKeys; ++import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; ++import org.apache.logging.log4j.core.pattern.PatternConverter; ++import org.apache.logging.log4j.core.pattern.PatternFormatter; ++import org.apache.logging.log4j.core.pattern.PatternParser; ++import org.apache.logging.log4j.util.PerformanceSensitive; ++ ++import java.util.List; ++ ++@Plugin(name = "highlightGUIError", category = PatternConverter.CATEGORY) ++@ConverterKeys({"highlightGUIError"}) ++@PerformanceSensitive("allocation") ++public final class HighlightErrorConverter extends LogEventPatternConverter { ++ private static final String ERROR = "\u00A74\u00A7l"; // Bold Red ++ private static final String WARN = "\u00A7e\u00A7l"; // Bold Yellow ++ ++ private final List formatters; ++ ++ private HighlightErrorConverter(List formatters) { ++ super("highlightGUIError", null); ++ this.formatters = formatters; ++ } ++ ++ @Override ++ public void format(LogEvent event, StringBuilder toAppendTo) { ++ Level level = event.getLevel(); ++ if (level.isMoreSpecificThan(Level.ERROR)) { ++ format(ERROR, event, toAppendTo); ++ return; ++ } else if (level.isMoreSpecificThan(Level.WARN)) { ++ format(WARN, event, toAppendTo); ++ return; ++ } ++ for (PatternFormatter formatter : formatters) { ++ formatter.format(event, toAppendTo); ++ } ++ } ++ ++ private void format(String style, LogEvent event, StringBuilder toAppendTo) { ++ int start = toAppendTo.length(); ++ toAppendTo.append(style); ++ int end = toAppendTo.length(); ++ ++ for (PatternFormatter formatter : formatters) { ++ formatter.format(event, toAppendTo); ++ } ++ ++ if (toAppendTo.length() == end) { ++ toAppendTo.setLength(start); ++ } ++ } ++ ++ @Override ++ public boolean handlesThrowable() { ++ for (final PatternFormatter formatter : formatters) { ++ if (formatter.handlesThrowable()) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public static HighlightErrorConverter newInstance(Configuration config, String[] options) { ++ if (options.length != 1) { ++ LOGGER.error("Incorrect number of options on highlightGUIError. Expected 1 received " + options.length); ++ return null; ++ } ++ ++ if (options[0] == null) { ++ LOGGER.error("No pattern supplied on highlightGUIError"); ++ return null; ++ } ++ ++ PatternParser parser = PatternLayout.createPatternParser(config); ++ List formatters = parser.parse(options[0]); ++ return new HighlightErrorConverter(formatters); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java b/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f526883495b3222746de3d0442e9e4fb5107036 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/item/GlowBerryItem.java +@@ -0,0 +1,26 @@ ++package org.purpurmc.purpur.item; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.effect.MobEffectInstance; ++import net.minecraft.world.effect.MobEffects; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.item.ItemNameBlockItem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import org.bukkit.event.entity.EntityPotionEffectEvent; ++ ++public class GlowBerryItem extends ItemNameBlockItem { ++ public GlowBerryItem(Block block, Properties settings) { ++ super(block, settings); ++ } ++ ++ @Override ++ public ItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user) { ++ ItemStack result = super.finishUsingItem(stack, world, user); ++ if (world.purpurConfig.glowBerriesEatGlowDuration > 0 && user instanceof ServerPlayer player) { ++ player.addEffect(new MobEffectInstance(MobEffects.GLOWING, world.purpurConfig.glowBerriesEatGlowDuration), EntityPotionEffectEvent.Cause.FOOD); ++ } ++ return result; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java b/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c038fb2bbb0f0e78380bc24bbd6348b869669a90 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/item/SpawnerItem.java +@@ -0,0 +1,36 @@ ++package org.purpurmc.purpur.item; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.BlockItem; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.SpawnerBlockEntity; ++import net.minecraft.world.level.block.state.BlockState; ++ ++public class SpawnerItem extends BlockItem { ++ ++ public SpawnerItem(Block block, Properties settings) { ++ super(block, settings); ++ } ++ ++ @Override ++ protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, Player player, ItemStack stack, BlockState state) { ++ boolean handled = super.updateCustomBlockEntityTag(pos, level, player, stack, state); ++ if (level.purpurConfig.silkTouchEnabled && player.getBukkitEntity().hasPermission("purpur.place.spawners")) { ++ BlockEntity spawner = level.getBlockEntity(pos); ++ if (spawner instanceof SpawnerBlockEntity && stack.hasTag()) { ++ CompoundTag tag = stack.getTag(); ++ if (tag.contains("Purpur.mob_type")) { ++ EntityType.byString(tag.getString("Purpur.mob_type")).ifPresent(type -> ++ ((SpawnerBlockEntity) spawner).getSpawner().setEntityId(type, level, level.random, pos)); ++ } ++ } ++ } ++ return handled; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java b/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..055dd307e9d5ac0d4623c961164c84bab1edd3bd +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/BeehiveTask.java +@@ -0,0 +1,81 @@ ++package org.purpurmc.purpur.task; ++ ++import com.google.common.io.ByteArrayDataInput; ++import com.google.common.io.ByteArrayDataOutput; ++import com.google.common.io.ByteStreams; ++import io.netty.buffer.Unpooled; ++import net.minecraft.core.BlockPos; ++import net.minecraft.network.FriendlyByteBuf; ++import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.block.entity.BeehiveBlockEntity; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; ++import org.bukkit.entity.Player; ++import org.bukkit.plugin.PluginBase; ++import org.bukkit.plugin.messaging.PluginMessageListener; ++import org.jetbrains.annotations.NotNull; ++ ++public class BeehiveTask implements PluginMessageListener { ++ public static final ResourceLocation BEEHIVE_C2S = new ResourceLocation("purpur", "beehive_c2s"); ++ public static final ResourceLocation BEEHIVE_S2C = new ResourceLocation("purpur", "beehive_s2c"); ++ ++ private static BeehiveTask instance; ++ ++ public static BeehiveTask instance() { ++ if (instance == null) { ++ instance = new BeehiveTask(); ++ } ++ return instance; ++ } ++ ++ private final PluginBase plugin = new MinecraftInternalPlugin(); ++ ++ private BeehiveTask() { ++ } ++ ++ public void register() { ++ Bukkit.getMessenger().registerOutgoingPluginChannel(this.plugin, BEEHIVE_S2C.toString()); ++ Bukkit.getMessenger().registerIncomingPluginChannel(this.plugin, BEEHIVE_C2S.toString(), this); ++ } ++ ++ public void unregister() { ++ Bukkit.getMessenger().unregisterOutgoingPluginChannel(this.plugin, BEEHIVE_S2C.toString()); ++ Bukkit.getMessenger().unregisterIncomingPluginChannel(this.plugin, BEEHIVE_C2S.toString()); ++ } ++ ++ @Override ++ public void onPluginMessageReceived(@NotNull String channel, Player player, byte[] bytes) { ++ ByteArrayDataInput in = in(bytes); ++ long packedPos = in.readLong(); ++ BlockPos pos = BlockPos.of(packedPos); ++ ++ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); ++ ++ BlockEntity blockEntity = serverPlayer.level.getBlockEntity(pos); ++ if (!(blockEntity instanceof BeehiveBlockEntity beehive)) { ++ return; ++ } ++ ++ ByteArrayDataOutput out = out(); ++ ++ out.writeInt(beehive.getOccupantCount()); ++ out.writeLong(packedPos); ++ ++ FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(out.toByteArray())); ++ serverPlayer.connection.send(new ClientboundCustomPayloadPacket(BEEHIVE_S2C, buf)); ++ } ++ ++ @SuppressWarnings("UnstableApiUsage") ++ private static ByteArrayDataOutput out() { ++ return ByteStreams.newDataOutput(); ++ } ++ ++ @SuppressWarnings("UnstableApiUsage") ++ private static ByteArrayDataInput in(byte[] bytes) { ++ return ByteStreams.newDataInput(bytes); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/BossBarTask.java b/src/main/java/org/purpurmc/purpur/task/BossBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..114f273dd7f8b8a3c02f0651f6944859b33a65d4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/BossBarTask.java +@@ -0,0 +1,121 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; ++import org.bukkit.entity.Player; ++import org.bukkit.scheduler.BukkitRunnable; ++ ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.Map; ++import java.util.UUID; ++ ++public abstract class BossBarTask extends BukkitRunnable { ++ private final Map bossbars = new HashMap<>(); ++ private boolean started; ++ ++ abstract BossBar createBossBar(); ++ ++ abstract void updateBossBar(BossBar bossbar, Player player); ++ ++ @Override ++ public void run() { ++ Iterator> iter = bossbars.entrySet().iterator(); ++ while (iter.hasNext()) { ++ Map.Entry entry = iter.next(); ++ Player player = Bukkit.getPlayer(entry.getKey()); ++ if (player == null) { ++ iter.remove(); ++ continue; ++ } ++ updateBossBar(entry.getValue(), player); ++ } ++ } ++ ++ @Override ++ public void cancel() { ++ super.cancel(); ++ new HashSet<>(this.bossbars.keySet()).forEach(uuid -> { ++ Player player = Bukkit.getPlayer(uuid); ++ if (player != null) { ++ removePlayer(player); ++ } ++ }); ++ this.bossbars.clear(); ++ } ++ ++ public boolean removePlayer(Player player) { ++ BossBar bossbar = this.bossbars.remove(player.getUniqueId()); ++ if (bossbar != null) { ++ player.hideBossBar(bossbar); ++ return true; ++ } ++ return false; ++ } ++ ++ public void addPlayer(Player player) { ++ removePlayer(player); ++ BossBar bossbar = createBossBar(); ++ this.bossbars.put(player.getUniqueId(), bossbar); ++ this.updateBossBar(bossbar, player); ++ player.showBossBar(bossbar); ++ } ++ ++ public boolean hasPlayer(UUID uuid) { ++ return this.bossbars.containsKey(uuid); ++ } ++ ++ public boolean togglePlayer(Player player) { ++ if (removePlayer(player)) { ++ return false; ++ } ++ addPlayer(player); ++ return true; ++ } ++ ++ public void start() { ++ stop(); ++ this.runTaskTimerAsynchronously(new MinecraftInternalPlugin(), 1, 1); ++ started = true; ++ } ++ ++ public void stop() { ++ if (started) { ++ cancel(); ++ } ++ } ++ ++ public static void startAll() { ++ RamBarTask.instance().start(); ++ TPSBarTask.instance().start(); ++ CompassTask.instance().start(); ++ } ++ ++ public static void stopAll() { ++ RamBarTask.instance().stop(); ++ TPSBarTask.instance().stop(); ++ CompassTask.instance().stop(); ++ } ++ ++ public static void addToAll(ServerPlayer player) { ++ Player bukkit = player.getBukkitEntity(); ++ if (player.ramBar()) { ++ RamBarTask.instance().addPlayer(bukkit); ++ } ++ if (player.tpsBar()) { ++ TPSBarTask.instance().addPlayer(bukkit); ++ } ++ if (player.compassBar()) { ++ CompassTask.instance().addPlayer(bukkit); ++ } ++ } ++ ++ public static void removeFromAll(Player player) { ++ RamBarTask.instance().removePlayer(player); ++ TPSBarTask.instance().removePlayer(player); ++ CompassTask.instance().removePlayer(player); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/CompassTask.java b/src/main/java/org/purpurmc/purpur/task/CompassTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bece7eefc8ba8822b433835526251d2fb916c025 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/CompassTask.java +@@ -0,0 +1,68 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.item.Items; ++import org.bukkit.entity.Player; ++import org.purpurmc.purpur.PurpurConfig; ++ ++public class CompassTask extends BossBarTask { ++ private static CompassTask instance; ++ ++ private int tick = 0; ++ ++ public static CompassTask instance() { ++ if (instance == null) { ++ instance = new CompassTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ public void run() { ++ if (++tick < PurpurConfig.commandCompassBarTickInterval) { ++ return; ++ } ++ tick = 0; ++ ++ MinecraftServer.getServer().getAllLevels().forEach((level) -> { ++ if (level.purpurConfig.compassItemShowsBossBar) { ++ level.players().forEach(player -> { ++ if (!player.compassBar()) { ++ if (player.getMainHandItem().getItem() != Items.COMPASS && player.getOffhandItem().getItem() != Items.COMPASS) { ++ removePlayer(player.getBukkitEntity()); ++ } else if (!hasPlayer(player.getUUID())) { ++ addPlayer(player.getBukkitEntity()); ++ } ++ } ++ }); ++ } ++ }); ++ ++ super.run(); ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), PurpurConfig.commandCompassBarProgressPercent, PurpurConfig.commandCompassBarProgressColor, PurpurConfig.commandCompassBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ float yaw = player.getLocation().getYaw(); ++ int length = PurpurConfig.commandCompassBarTitle.length(); ++ int pos = (int) ((normalize(yaw) * (length / 720F)) + (length / 2F)); ++ bossbar.name(Component.text(PurpurConfig.commandCompassBarTitle.substring(pos - 25, pos + 25))); ++ } ++ ++ private float normalize(float yaw) { ++ while (yaw < -180.0F) { ++ yaw += 360.0F; ++ } ++ while (yaw > 180.0F) { ++ yaw -= 360.0F; ++ } ++ return yaw; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/RamBarTask.java b/src/main/java/org/purpurmc/purpur/task/RamBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e98c0ae73e2c40002a72b5d0d246ffa0c3ab38f +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/RamBarTask.java +@@ -0,0 +1,117 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import org.bukkit.entity.Player; ++import org.purpurmc.purpur.PurpurConfig; ++ ++import java.lang.management.ManagementFactory; ++import java.lang.management.MemoryUsage; ++ ++public class RamBarTask extends BossBarTask { ++ private static RamBarTask instance; ++ private long allocated = 0L; ++ private long used = 0L; ++ private long xmx = 0L; ++ private long xms = 0L; ++ private float percent = 0F; ++ private int tick = 0; ++ ++ public static RamBarTask instance() { ++ if (instance == null) { ++ instance = new RamBarTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), 0.0F, instance().getBossBarColor(), PurpurConfig.commandRamBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ bossbar.progress(getBossBarProgress()); ++ bossbar.color(getBossBarColor()); ++ bossbar.name(MiniMessage.miniMessage().deserialize(PurpurConfig.commandRamBarTitle, ++ Placeholder.component("allocated", format(this.allocated)), ++ Placeholder.component("used", format(this.used)), ++ Placeholder.component("xmx", format(this.xmx)), ++ Placeholder.component("xms", format(this.xms)), ++ Placeholder.unparsed("percent", ((int) (this.percent * 100)) + "%") ++ )); ++ } ++ ++ @Override ++ public void run() { ++ if (++this.tick < PurpurConfig.commandRamBarTickInterval) { ++ return; ++ } ++ this.tick = 0; ++ ++ MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); ++ ++ this.allocated = heap.getCommitted(); ++ this.used = heap.getUsed(); ++ this.xmx = heap.getMax(); ++ this.xms = heap.getInit(); ++ this.percent = Math.max(Math.min((float) this.used / this.xmx, 1.0F), 0.0F); ++ ++ super.run(); ++ } ++ ++ private float getBossBarProgress() { ++ return this.percent; ++ } ++ ++ private BossBar.Color getBossBarColor() { ++ if (this.percent < 0.5F) { ++ return PurpurConfig.commandRamBarProgressColorGood; ++ } else if (this.percent < 0.75F) { ++ return PurpurConfig.commandRamBarProgressColorMedium; ++ } else { ++ return PurpurConfig.commandRamBarProgressColorLow; ++ } ++ } ++ ++ public Component format(long v) { ++ String color; ++ if (this.percent < 0.60F) { ++ color = PurpurConfig.commandRamBarTextColorGood; ++ } else if (this.percent < 0.85F) { ++ color = PurpurConfig.commandRamBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandRamBarTextColorLow; ++ } ++ String value; ++ if (v < 1024) { ++ value = v + "B"; ++ } else { ++ int z = (63 - Long.numberOfLeadingZeros(v)) / 10; ++ value = String.format("%.1f%s", (double) v / (1L << (z * 10)), "BKMGTPE".charAt(z)); ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.unparsed("text", value)); ++ } ++ ++ public long getAllocated() { ++ return this.allocated; ++ } ++ ++ public long getUsed() { ++ return this.used; ++ } ++ ++ public long getXmx() { ++ return this.xmx; ++ } ++ ++ public long getXms() { ++ return this.xms; ++ } ++ ++ public float getPercent() { ++ return this.percent; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java b/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8769993e7ca59da309087051a3cd38fc562c15d1 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/task/TPSBarTask.java +@@ -0,0 +1,142 @@ ++package org.purpurmc.purpur.task; ++ ++import net.kyori.adventure.bossbar.BossBar; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.minimessage.MiniMessage; ++import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; ++import org.purpurmc.purpur.PurpurConfig; ++import org.bukkit.Bukkit; ++import org.bukkit.entity.Player; ++ ++public class TPSBarTask extends BossBarTask { ++ private static TPSBarTask instance; ++ private double tps = 20.0D; ++ private double mspt = 0.0D; ++ private int tick = 0; ++ ++ public static TPSBarTask instance() { ++ if (instance == null) { ++ instance = new TPSBarTask(); ++ } ++ return instance; ++ } ++ ++ @Override ++ BossBar createBossBar() { ++ return BossBar.bossBar(Component.text(""), 0.0F, instance().getBossBarColor(), PurpurConfig.commandTPSBarProgressOverlay); ++ } ++ ++ @Override ++ void updateBossBar(BossBar bossbar, Player player) { ++ bossbar.progress(getBossBarProgress()); ++ bossbar.color(getBossBarColor()); ++ bossbar.name(MiniMessage.miniMessage().deserialize(PurpurConfig.commandTPSBarTitle, ++ Placeholder.component("tps", getTPSColor()), ++ Placeholder.component("mspt", getMSPTColor()), ++ Placeholder.component("ping", getPingColor(player.getPing())) ++ )); ++ } ++ ++ @Override ++ public void run() { ++ if (++tick < PurpurConfig.commandTPSBarTickInterval) { ++ return; ++ } ++ tick = 0; ++ ++ this.tps = Math.max(Math.min(Bukkit.getTPS()[0], 20.0D), 0.0D); ++ this.mspt = Bukkit.getAverageTickTime(); ++ ++ super.run(); ++ } ++ ++ private float getBossBarProgress() { ++ if (PurpurConfig.commandTPSBarProgressFillMode == FillMode.MSPT) { ++ return Math.max(Math.min((float) mspt / 50.0F, 1.0F), 0.0F); ++ } else { ++ return Math.max(Math.min((float) tps / 20.0F, 1.0F), 0.0F); ++ } ++ } ++ ++ private BossBar.Color getBossBarColor() { ++ if (isGood(PurpurConfig.commandTPSBarProgressFillMode)) { ++ return PurpurConfig.commandTPSBarProgressColorGood; ++ } else if (isMedium(PurpurConfig.commandTPSBarProgressFillMode)) { ++ return PurpurConfig.commandTPSBarProgressColorMedium; ++ } else { ++ return PurpurConfig.commandTPSBarProgressColorLow; ++ } ++ } ++ ++ private boolean isGood(FillMode mode) { ++ return isGood(mode, 0); ++ } ++ ++ private boolean isGood(FillMode mode, int ping) { ++ if (mode == FillMode.MSPT) { ++ return mspt < 40; ++ } else if (mode == FillMode.TPS) { ++ return tps >= 19; ++ } else if (mode == FillMode.PING) { ++ return ping < 100; ++ } else { ++ return false; ++ } ++ } ++ ++ private boolean isMedium(FillMode mode) { ++ return isMedium(mode, 0); ++ } ++ ++ private boolean isMedium(FillMode mode, int ping) { ++ if (mode == FillMode.MSPT) { ++ return mspt < 50; ++ } else if (mode == FillMode.TPS) { ++ return tps >= 15; ++ } else if (mode == FillMode.PING) { ++ return ping < 200; ++ } else { ++ return false; ++ } ++ } ++ ++ private Component getTPSColor() { ++ String color; ++ if (isGood(FillMode.TPS)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.TPS)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%.2f", tps))); ++ } ++ ++ private Component getMSPTColor() { ++ String color; ++ if (isGood(FillMode.MSPT)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.MSPT)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%.2f", mspt))); ++ } ++ ++ private Component getPingColor(int ping) { ++ String color; ++ if (isGood(FillMode.PING, ping)) { ++ color = PurpurConfig.commandTPSBarTextColorGood; ++ } else if (isMedium(FillMode.PING, ping)) { ++ color = PurpurConfig.commandTPSBarTextColorMedium; ++ } else { ++ color = PurpurConfig.commandTPSBarTextColorLow; ++ } ++ return MiniMessage.miniMessage().deserialize(color, Placeholder.parsed("text", String.format("%s", ping))); ++ } ++ ++ public enum FillMode { ++ TPS, MSPT, PING ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Actionable.java b/src/main/java/org/purpurmc/purpur/tool/Actionable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e18c37f06730da9d3055d5215e813b1477c1e70e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Actionable.java +@@ -0,0 +1,24 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public abstract class Actionable { ++ private final Block into; ++ private final Map drops; ++ ++ public Actionable(Block into, Map drops) { ++ this.into = into; ++ this.drops = drops; ++ } ++ ++ public Block into() { ++ return into; ++ } ++ ++ public Map drops() { ++ return drops; ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Strippable.java b/src/main/java/org/purpurmc/purpur/tool/Strippable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bf5402214f41af9c09bd6c5c4f45d330516d742e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Strippable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Strippable extends Actionable { ++ public Strippable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Tillable.java b/src/main/java/org/purpurmc/purpur/tool/Tillable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..715f6dd44480347eebced43c11bc364e05727498 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Tillable.java +@@ -0,0 +1,50 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.HoeItem; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.context.UseOnContext; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Predicate; ++ ++public class Tillable extends Actionable { ++ private final Condition condition; ++ ++ public Tillable(Condition condition, Block into, Map drops) { ++ super(into, drops); ++ this.condition = condition; ++ } ++ ++ public Condition condition() { ++ return condition; ++ } ++ ++ public enum Condition { ++ AIR_ABOVE(HoeItem::onlyIfAirAbove), ++ ALWAYS((useOnContext) -> true); ++ ++ private final Predicate predicate; ++ ++ Condition(Predicate predicate) { ++ this.predicate = predicate; ++ } ++ ++ public Predicate predicate() { ++ return predicate; ++ } ++ ++ private static final Map BY_NAME = new HashMap<>(); ++ ++ static { ++ for (Condition condition : values()) { ++ BY_NAME.put(condition.name(), condition); ++ } ++ } ++ ++ public static Condition get(String name) { ++ return BY_NAME.get(name.toUpperCase(java.util.Locale.ROOT)); ++ } ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Waxable.java b/src/main/java/org/purpurmc/purpur/tool/Waxable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64adb13b29b6757dcf227a55588da70ecabe083f +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Waxable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Waxable extends Actionable { ++ public Waxable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/tool/Weatherable.java b/src/main/java/org/purpurmc/purpur/tool/Weatherable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b7586f494528f30eb0da82420d3bcf5b83a1a902 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/tool/Weatherable.java +@@ -0,0 +1,12 @@ ++package org.purpurmc.purpur.tool; ++ ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++ ++import java.util.Map; ++ ++public class Weatherable extends Actionable { ++ public Weatherable(Block into, Map drops) { ++ super(into, drops); ++ } ++} +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index 07050c78a2eb6ce0c699101b38961b111d631a41..ee64ddb0da23ea1e54d0295324aca5b46a438111 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -15,6 +15,7 @@ import net.minecraft.world.entity.ambient.AmbientCreature; + import net.minecraft.world.entity.animal.Animal; + import net.minecraft.world.entity.animal.Bee; + import net.minecraft.world.entity.animal.Sheep; ++import net.minecraft.world.entity.animal.Squid; + import net.minecraft.world.entity.animal.WaterAnimal; + import net.minecraft.world.entity.animal.horse.Llama; + import net.minecraft.world.entity.boss.EnderDragonPart; +@@ -169,7 +170,7 @@ public class ActivationRange + */ + public static void activateEntities(Level world) + { +- MinecraftTimings.entityActivationCheckTimer.startTiming(); ++ //MinecraftTimings.entityActivationCheckTimer.startTiming(); // Purpur + final int miscActivationRange = world.spigotConfig.miscActivationRange; + final int raiderActivationRange = world.spigotConfig.raiderActivationRange; + final int animalActivationRange = world.spigotConfig.animalActivationRange; +@@ -203,6 +204,7 @@ public class ActivationRange + continue; + } + ++ if (!player.level.purpurConfig.idleTimeoutTickNearbyEntities && player.isAfk()) continue; // Purpur + // Paper start + int worldHeight = world.getHeight(); + ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); +@@ -243,7 +245,7 @@ public class ActivationRange + } + // Paper end + } +- MinecraftTimings.entityActivationCheckTimer.stopTiming(); ++ //MinecraftTimings.entityActivationCheckTimer.stopTiming(); // Purpur + } + + /** +@@ -396,6 +398,7 @@ public class ActivationRange + */ + public static boolean checkIfActive(Entity entity) + { ++ if (entity.level.purpurConfig.squidImmuneToEAR && entity instanceof Squid) return true; // Purpur + // Never safe to skip fireworks or entities not yet added to chunk + if ( entity instanceof FireworkRocketEntity ) { + return true; +diff --git a/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +index bf970bf3356a914459c2d6db93537ce2d32c7e18..08221c7256f41ca511a4a61ffb2b979325aebee9 100644 +--- a/src/main/java/org/spigotmc/TicksPerSecondCommand.java ++++ b/src/main/java/org/spigotmc/TicksPerSecondCommand.java +@@ -39,7 +39,7 @@ public class TicksPerSecondCommand extends Command + } + + net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text(); +- builder.append(net.kyori.adventure.text.Component.text("TPS from last 1m, 5m, 15m: ", net.kyori.adventure.text.format.NamedTextColor.GOLD)); ++ builder.append(net.kyori.adventure.text.Component.text("TPS from last 5s, 1m, 5m, 15m: ", net.kyori.adventure.text.format.NamedTextColor.GOLD)); + builder.append(net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.commas(true), tpsAvg)); + sender.sendMessage(builder.asComponent()); + if (args.length > 0 && args[0].equals("mem") && sender.hasPermission("bukkit.command.tpsmemory")) { +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 74ccc67e3c12dc5182602fb691ef3ddeb5b53280..52af11926a1f7973d70a1dae191d2e8138ec5c94 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -2,7 +2,16 @@ + + + +- ++ ++ ++ ++ ++ ++ ++ ++ ++ + + + +diff --git a/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java +index b2d510459bcf90a3611f3d91dae4ccc3d29b4079..7a052f6deaa30f8a177a2aaf172f9da6c308a22b 100644 +--- a/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java ++++ b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java +@@ -37,7 +37,7 @@ public class VanillaMobGoalTest { + } + + List> classes; +- try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft").scan()) { ++ try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft", "org.purpurmc.purpur.entity.ai").scan()) { // Purpur + classes = scanResult.getSubclasses(net.minecraft.world.entity.ai.goal.Goal.class.getName()).loadClasses(); + } + +diff --git a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +index 8665e2740aedcc2895b0e2c44ebaba53d2a40568..b7e2227116ee0a08826674d8681fdaac97efb0ea 100644 +--- a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java ++++ b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +@@ -45,6 +45,7 @@ public class MinecraftCommandPermissionsTest extends AbstractTestingBase { + Set foundPerms = new HashSet<>(); + for (CommandNode child : root.getChildren()) { + final String vanillaPerm = VanillaCommandWrapper.getPermission(child); ++ if (TO_SKIP.contains(vanillaPerm)) continue; // Purpur + if (!perms.contains(vanillaPerm)) { + missing.add("Missing permission for " + child.getName() + " (" + vanillaPerm + ") command"); + } else { +@@ -57,6 +58,25 @@ public class MinecraftCommandPermissionsTest extends AbstractTestingBase { + } + + private static final List TO_SKIP = List.of( ++ // Purpur start ++ "minecraft.command.compass", ++ "minecraft.command.credits", ++ "minecraft.command.demo", ++ "minecraft.command.ping", ++ "minecraft.command.ram", ++ "minecraft.command.rambar", ++ "minecraft.command.tpsbar", ++ "minecraft.command.uptime", ++ "minecraft.command.debug", ++ "minecraft.command.gamemode.adventure", ++ "minecraft.command.gamemode.adventure.other", ++ "minecraft.command.gamemode.creative", ++ "minecraft.command.gamemode.creative.other", ++ "minecraft.command.gamemode.spectator", ++ "minecraft.command.gamemode.spectator.other", ++ "minecraft.command.gamemode.survival", ++ "minecraft.command.gamemode.survival.other", ++ // Purpur end + "minecraft.command.selector" + ); + +diff --git a/src/test/java/org/bukkit/potion/PotionTest.java b/src/test/java/org/bukkit/potion/PotionTest.java +index 83226ec2fa977819e12a499eb3765232543c17b3..a742774dabaee0629f4e6adabee5f3ec4b3be41c 100644 +--- a/src/test/java/org/bukkit/potion/PotionTest.java ++++ b/src/test/java/org/bukkit/potion/PotionTest.java +@@ -9,6 +9,7 @@ import net.minecraft.resources.ResourceLocation; + import net.minecraft.world.effect.MobEffect; + import net.minecraft.world.effect.MobEffectInstance; + import net.minecraft.world.item.alchemy.Potion; ++import org.bukkit.NamespacedKey; + import org.bukkit.support.AbstractTestingBase; + import org.junit.Test; + +@@ -47,4 +48,27 @@ public class PotionTest extends AbstractTestingBase { + assertEquals("Same type not returned by name " + key, bukkit, byName); + } + } ++ ++ // Purpur start ++ @Test ++ public void testNamespacedKey() { ++ NamespacedKey key = new NamespacedKey("testnamespace", "testkey"); ++ PotionEffect namedSpacedEffect = new PotionEffect(PotionEffectType.DOLPHINS_GRACE, 20, 0, true, true, true, key); ++ assertNotNull(namedSpacedEffect.getKey()); ++ assertTrue(namedSpacedEffect.hasKey()); ++ assertFalse(namedSpacedEffect.withKey(null).hasKey()); ++ ++ PotionEffect effect = new PotionEffect(PotionEffectType.DOLPHINS_GRACE, 20, 0, true, true, true); ++ assertNull(effect.getKey()); ++ assertFalse(effect.hasKey()); ++ assertTrue(namedSpacedEffect.withKey(key).hasKey()); ++ ++ Map s1 = namedSpacedEffect.serialize(); ++ Map s2 = effect.serialize(); ++ assertTrue(s1.containsKey("namespacedKey")); ++ assertFalse(s2.containsKey("namespacedKey")); ++ assertNotNull(new PotionEffect(s1).getKey()); ++ assertNull(new PotionEffect(s2).getKey()); ++ } ++ // Purpur end + } diff --git a/patches/server/0008-Bump-Dependencies.patch b/patches/server/0009-Bump-Dependencies.patch similarity index 93% rename from patches/server/0008-Bump-Dependencies.patch rename to patches/server/0009-Bump-Dependencies.patch index 8d3f1925..8f9051d4 100644 --- a/patches/server/0008-Bump-Dependencies.patch +++ b/patches/server/0009-Bump-Dependencies.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Bump Dependencies diff --git a/build.gradle.kts b/build.gradle.kts -index 493acdeeb56ed13a0c30d9d10b2f9171df296d98..ba66cd2f91ba69d6e10d542610a58fd2b403671c 100644 +index 3c1e97fb031f7cdf73ecb6cf8ec662e08b78f96f..12b91410d3f0afd5882960563bf54b59e8c42b5e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,13 +11,13 @@ dependencies { @@ -25,7 +25,7 @@ index 493acdeeb56ed13a0c30d9d10b2f9171df296d98..ba66cd2f91ba69d6e10d542610a58fd2 implementation("net.minecrell:terminalconsoleappender:1.3.0") /* Required to add the missing Log4j2Plugins.dat file from log4j-core -@@ -25,42 +25,47 @@ dependencies { +@@ -25,22 +25,24 @@ dependencies { all its classes to check if they are plugins. Scanning takes about 1-2 seconds so adding this speeds up the server start. */ @@ -59,6 +59,9 @@ index 493acdeeb56ed13a0c30d9d10b2f9171df296d98..ba66cd2f91ba69d6e10d542610a58fd2 isTransitive = false } // Paper end +@@ -49,22 +51,25 @@ dependencies { + implementation("org.mozilla:rhino-engine:1.7.14") // Purpur + implementation("dev.omega24:upnp4j:1.0") // Purpur - runtimeOnly("org.apache.maven:maven-resolver-provider:3.8.5") - runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.3") @@ -89,7 +92,7 @@ index 493acdeeb56ed13a0c30d9d10b2f9171df296d98..ba66cd2f91ba69d6e10d542610a58fd2 } val craftbukkitPackageVersion = "1_19_R3" // Paper -@@ -227,3 +232,6 @@ tasks.registerRunTask("runDev") { +@@ -231,3 +236,6 @@ tasks.registerRunTask("runDev") { description = "Spin up a non-relocated Mojang-mapped test server" classpath(sourceSets.main.map { it.runtimeClasspath }) } diff --git a/patches/server/0009-Remove-Mojang-username-check.patch b/patches/server/0010-Remove-Mojang-username-check.patch similarity index 100% rename from patches/server/0009-Remove-Mojang-username-check.patch rename to patches/server/0010-Remove-Mojang-username-check.patch diff --git a/patches/server/0010-Remove-Spigot-Check-for-Broken-BungeeCord-Configurat.patch b/patches/server/0011-Remove-Spigot-Check-for-Broken-BungeeCord-Configurat.patch similarity index 100% rename from patches/server/0010-Remove-Spigot-Check-for-Broken-BungeeCord-Configurat.patch rename to patches/server/0011-Remove-Spigot-Check-for-Broken-BungeeCord-Configurat.patch diff --git a/patches/server/0011-Remove-Paper-s-Fix-tripwire-state-inconsistency.patch b/patches/server/0012-Remove-Paper-s-Fix-tripwire-state-inconsistency.patch similarity index 100% rename from patches/server/0011-Remove-Paper-s-Fix-tripwire-state-inconsistency.patch rename to patches/server/0012-Remove-Paper-s-Fix-tripwire-state-inconsistency.patch diff --git a/patches/server/0012-Remove-UseItemOnPacket-Too-Far-Check.patch b/patches/server/0013-Remove-UseItemOnPacket-Too-Far-Check.patch similarity index 100% rename from patches/server/0012-Remove-UseItemOnPacket-Too-Far-Check.patch rename to patches/server/0013-Remove-UseItemOnPacket-Too-Far-Check.patch diff --git a/patches/server/0013-KTP-Optimize-spigot-event-bus.patch b/patches/server/0014-KTP-Optimize-spigot-event-bus.patch similarity index 100% rename from patches/server/0013-KTP-Optimize-spigot-event-bus.patch rename to patches/server/0014-KTP-Optimize-spigot-event-bus.patch diff --git a/patches/server/0014-KeYi-Player-Skull-API.patch b/patches/server/0015-KeYi-Player-Skull-API.patch similarity index 100% rename from patches/server/0014-KeYi-Player-Skull-API.patch rename to patches/server/0015-KeYi-Player-Skull-API.patch diff --git a/patches/server/0015-KeYi-Disable-arrow-despawn-counter-by-default.patch b/patches/server/0016-KeYi-Disable-arrow-despawn-counter-by-default.patch similarity index 100% rename from patches/server/0015-KeYi-Disable-arrow-despawn-counter-by-default.patch rename to patches/server/0016-KeYi-Disable-arrow-despawn-counter-by-default.patch diff --git a/patches/server/0016-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch b/patches/server/0017-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch similarity index 100% rename from patches/server/0016-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch rename to patches/server/0017-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch diff --git a/patches/server/0017-Carpet-Fixes-Optimized-getBiome-method.patch b/patches/server/0018-Carpet-Fixes-Optimized-getBiome-method.patch similarity index 100% rename from patches/server/0017-Carpet-Fixes-Optimized-getBiome-method.patch rename to patches/server/0018-Carpet-Fixes-Optimized-getBiome-method.patch diff --git a/patches/server/0018-Carpet-Fixes-Use-optimized-RecipeManager.patch b/patches/server/0019-Carpet-Fixes-Use-optimized-RecipeManager.patch similarity index 100% rename from patches/server/0018-Carpet-Fixes-Use-optimized-RecipeManager.patch rename to patches/server/0019-Carpet-Fixes-Use-optimized-RecipeManager.patch diff --git a/patches/server/0019-Petal-reduce-work-done-by-game-event-system.patch b/patches/server/0020-Petal-reduce-work-done-by-game-event-system.patch similarity index 100% rename from patches/server/0019-Petal-reduce-work-done-by-game-event-system.patch rename to patches/server/0020-Petal-reduce-work-done-by-game-event-system.patch diff --git a/patches/server/0020-Petal-Reduce-sensor-work.patch b/patches/server/0021-Petal-Reduce-sensor-work.patch similarity index 100% rename from patches/server/0020-Petal-Reduce-sensor-work.patch rename to patches/server/0021-Petal-Reduce-sensor-work.patch diff --git a/patches/server/0021-Akarin-Save-Json-list-asynchronously.patch b/patches/server/0022-Akarin-Save-Json-list-asynchronously.patch similarity index 100% rename from patches/server/0021-Akarin-Save-Json-list-asynchronously.patch rename to patches/server/0022-Akarin-Save-Json-list-asynchronously.patch diff --git a/patches/server/0022-Slice-Smooth-Teleports.patch b/patches/server/0023-Slice-Smooth-Teleports.patch similarity index 100% rename from patches/server/0022-Slice-Smooth-Teleports.patch rename to patches/server/0023-Slice-Smooth-Teleports.patch diff --git a/patches/server/0023-PaperPR-Optimize-VarInts.patch b/patches/server/0024-PaperPR-Optimize-VarInts.patch similarity index 100% rename from patches/server/0023-PaperPR-Optimize-VarInts.patch rename to patches/server/0024-PaperPR-Optimize-VarInts.patch diff --git a/patches/server/0024-Parchment-Make-FixLight-use-action-bar.patch b/patches/server/0025-Parchment-Make-FixLight-use-action-bar.patch similarity index 100% rename from patches/server/0024-Parchment-Make-FixLight-use-action-bar.patch rename to patches/server/0025-Parchment-Make-FixLight-use-action-bar.patch diff --git a/patches/server/0025-Leaves-Server-Utils.patch b/patches/server/0026-Leaves-Server-Utils.patch similarity index 100% rename from patches/server/0025-Leaves-Server-Utils.patch rename to patches/server/0026-Leaves-Server-Utils.patch diff --git a/patches/server/0026-Leaves-Jade-Protocol.patch b/patches/server/0027-Leaves-Jade-Protocol.patch similarity index 100% rename from patches/server/0026-Leaves-Jade-Protocol.patch rename to patches/server/0027-Leaves-Jade-Protocol.patch diff --git a/patches/server/0027-Leaves-Appleskin-Protocol.patch b/patches/server/0028-Leaves-Appleskin-Protocol.patch similarity index 97% rename from patches/server/0027-Leaves-Appleskin-Protocol.patch rename to patches/server/0028-Leaves-Appleskin-Protocol.patch index e359eefd..4bfe5a8a 100644 --- a/patches/server/0027-Leaves-Appleskin-Protocol.patch +++ b/patches/server/0028-Leaves-Appleskin-Protocol.patch @@ -7,7 +7,7 @@ Original license: GPLv3 Original project: https://github.com/LeavesMC/Leaves diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 2c23f420bcfecbc3d1b5edabacee981c6ff2349e..356c5bdf6257be75f40aca6099661096d078b9e2 100644 +index b39d799026efbfaf659471e58cfaa9210f0902b9..6318194ba2a9148f4c2798385febfaab4ace094b 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1612,6 +1612,7 @@ public abstract class MinecraftServer extends MinecraftServerBlockableEventLoop @@ -70,7 +70,7 @@ index 1718e70b56898865d41846a60cf4c514f8b0ee13..14121b31c8918318b7936ca74f708a20 // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) diff --git a/src/main/java/org/dreeam/leaf/LeafConfig.java b/src/main/java/org/dreeam/leaf/LeafConfig.java -index 657e5ab36a985da5945776cfe797cdfddd826f10..c9144eca8c6b59dfb37f9cc3f8f95ff65c3d75bc 100644 +index f77a8b729846dc9be3adad659ccc02df0467cd9e..2e3a7d57996532259d9559da9451df9fd58e9ccb 100644 --- a/src/main/java/org/dreeam/leaf/LeafConfig.java +++ b/src/main/java/org/dreeam/leaf/LeafConfig.java @@ -250,7 +250,9 @@ public class LeafConfig { diff --git a/patches/server/0028-Leaves-Xaero-Map-Protocol.patch b/patches/server/0029-Leaves-Xaero-Map-Protocol.patch similarity index 97% rename from patches/server/0028-Leaves-Xaero-Map-Protocol.patch rename to patches/server/0029-Leaves-Xaero-Map-Protocol.patch index ca1bbd32..b4c1152f 100644 --- a/patches/server/0028-Leaves-Xaero-Map-Protocol.patch +++ b/patches/server/0029-Leaves-Xaero-Map-Protocol.patch @@ -19,7 +19,7 @@ index 43ee9e37b4ac640df35de60b1496a52e81896321..a126c05b99916bf308982d57212957b3 // CraftBukkit start - handle player weather // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.START_RAINING, 0.0F)); diff --git a/src/main/java/org/dreeam/leaf/LeafConfig.java b/src/main/java/org/dreeam/leaf/LeafConfig.java -index c9144eca8c6b59dfb37f9cc3f8f95ff65c3d75bc..6a11a916281d86b1e7f29dfa394ae1a4575b27fa 100644 +index 2e3a7d57996532259d9559da9451df9fd58e9ccb..076234351d6a6647f017ed4e46a0554ae6053151 100644 --- a/src/main/java/org/dreeam/leaf/LeafConfig.java +++ b/src/main/java/org/dreeam/leaf/LeafConfig.java @@ -19,6 +19,7 @@ import java.util.Collections; diff --git a/patches/server/0029-Fix-Make-log4j-compatible-with-future-release.patch b/patches/server/0030-Fix-Make-log4j-compatible-with-future-release.patch similarity index 100% rename from patches/server/0029-Fix-Make-log4j-compatible-with-future-release.patch rename to patches/server/0030-Fix-Make-log4j-compatible-with-future-release.patch