From 2dea4ceae0779368b98d61b4aae566a966ab72ee Mon Sep 17 00:00:00 2001 From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com> Date: Fri, 16 Jun 2023 18:09:52 +0800 Subject: [PATCH] [ci skip] Readd Purpur Server --- patches/api/0002-Purpur-API-Changes.patch | 18 +- patches/api/0004-Bump-Dependencies.patch | 6 +- patches/server/0008-Purpur-Base.patch | 100 + .../server/0009-Purpur-Server-Changes.patch | 24678 ++++++++++++++++ ...ies.patch => 0010-Bump-Dependencies.patch} | 6 +- ...> 0011-Remove-Mojang-username-check.patch} | 4 +- ...ck-for-Broken-BungeeCord-Configurat.patch} | 0 ...-s-Fix-tripwire-state-inconsistency.patch} | 0 ...emove-UseItemOnPacket-Too-Far-Check.patch} | 4 +- ... 0015-KTP-Optimize-spigot-event-bus.patch} | 0 ...patch => 0016-KeYi-Player-Skull-API.patch} | 8 +- ...le-arrow-despawn-counter-by-default.patch} | 0 ...n-for-spigot-item-merging-mechanism.patch} | 4 +- ...pet-Fixes-Optimized-getBiome-method.patch} | 0 ...t-Fixes-Use-optimized-RecipeManager.patch} | 0 ...ch => 0021-Petal-Reduce-sensor-work.patch} | 31 +- ...karin-Save-Json-list-asynchronously.patch} | 0 ...atch => 0023-Slice-Smooth-Teleports.patch} | 18 +- ...ch => 0024-PaperPR-Optimize-VarInts.patch} | 6 +- ...chment-Make-FixLight-use-action-bar.patch} | 0 ...s.patch => 0026-Leaves-Server-Utils.patch} | 24 +- ....patch => 0027-Leaves-Jade-Protocol.patch} | 18 +- ...h => 0028-Leaves-Appleskin-Protocol.patch} | 14 +- ...h => 0029-Leaves-Xaero-Map-Protocol.patch} | 4 +- ...og4j-compatible-with-future-release.patch} | 4 +- ...ror.patch => 0031-Fix-compile-error.patch} | 0 26 files changed, 24863 insertions(+), 84 deletions(-) create mode 100644 patches/server/0008-Purpur-Base.patch create mode 100644 patches/server/0009-Purpur-Server-Changes.patch rename patches/server/{0008-Bump-Dependencies.patch => 0010-Bump-Dependencies.patch} (95%) rename patches/server/{0009-Remove-Mojang-username-check.patch => 0011-Remove-Mojang-username-check.patch} (95%) rename patches/server/{0010-Remove-Spigot-Check-for-Broken-BungeeCord-Configurat.patch => 0012-Remove-Spigot-Check-for-Broken-BungeeCord-Configurat.patch} (100%) rename patches/server/{0011-Remove-Paper-s-Fix-tripwire-state-inconsistency.patch => 0013-Remove-Paper-s-Fix-tripwire-state-inconsistency.patch} (100%) rename patches/server/{0012-Remove-UseItemOnPacket-Too-Far-Check.patch => 0014-Remove-UseItemOnPacket-Too-Far-Check.patch} (95%) rename patches/server/{0013-KTP-Optimize-spigot-event-bus.patch => 0015-KTP-Optimize-spigot-event-bus.patch} (100%) rename patches/server/{0014-KeYi-Player-Skull-API.patch => 0016-KeYi-Player-Skull-API.patch} (84%) rename patches/server/{0015-KeYi-Disable-arrow-despawn-counter-by-default.patch => 0017-KeYi-Disable-arrow-despawn-counter-by-default.patch} (100%) rename patches/server/{0016-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch => 0018-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch} (94%) rename patches/server/{0017-Carpet-Fixes-Optimized-getBiome-method.patch => 0019-Carpet-Fixes-Optimized-getBiome-method.patch} (100%) rename patches/server/{0018-Carpet-Fixes-Use-optimized-RecipeManager.patch => 0020-Carpet-Fixes-Use-optimized-RecipeManager.patch} (100%) rename patches/server/{0019-Petal-Reduce-sensor-work.patch => 0021-Petal-Reduce-sensor-work.patch} (55%) rename patches/server/{0020-Akarin-Save-Json-list-asynchronously.patch => 0022-Akarin-Save-Json-list-asynchronously.patch} (100%) rename patches/server/{0021-Slice-Smooth-Teleports.patch => 0023-Slice-Smooth-Teleports.patch} (82%) rename patches/server/{0022-PaperPR-Optimize-VarInts.patch => 0024-PaperPR-Optimize-VarInts.patch} (90%) rename patches/server/{0023-Parchment-Make-FixLight-use-action-bar.patch => 0025-Parchment-Make-FixLight-use-action-bar.patch} (100%) rename patches/server/{0024-Leaves-Server-Utils.patch => 0026-Leaves-Server-Utils.patch} (94%) rename patches/server/{0025-Leaves-Jade-Protocol.patch => 0027-Leaves-Jade-Protocol.patch} (97%) rename patches/server/{0026-Leaves-Appleskin-Protocol.patch => 0028-Leaves-Appleskin-Protocol.patch} (93%) rename patches/server/{0027-Leaves-Xaero-Map-Protocol.patch => 0029-Leaves-Xaero-Map-Protocol.patch} (96%) rename patches/server/{0028-Fix-Make-log4j-compatible-with-future-release.patch => 0030-Fix-Make-log4j-compatible-with-future-release.patch} (82%) rename patches/server/{0029-Fix-compile-error.patch => 0031-Fix-compile-error.patch} (100%) diff --git a/patches/api/0002-Purpur-API-Changes.patch b/patches/api/0002-Purpur-API-Changes.patch index fa47e734..a416ba0c 100644 --- a/patches/api/0002-Purpur-API-Changes.patch +++ b/patches/api/0002-Purpur-API-Changes.patch @@ -5,25 +5,13 @@ Subject: [PATCH] Purpur API Changes Commit: 2e5dcc63d331fe121d18f37dcecc34be17bf7abb -Changes from original Purpur: -Remove Remove timings patch +Patches below are removed in this patch: +Build-System-Changes.patch +Remove-Timings.patch Original license: MIT Original project: https://github.com/PurpurMC/Purpur -diff --git a/build.gradle.kts b/build.gradle.kts -index 1f9ed00eb14b2c25bab7f067644eefb4306abab4..31ef38465298b0bfc5c97810a8e3fb8ce8c466f3 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -110,6 +110,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/com/destroystokyo/paper/entity/ai/VanillaGoal.java b/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java index b7a2cecb334ce39fa09d8ab949a29eedbdc44c36..b1f35c68373edfe666ca05b50f0ec022a1859ce9 100644 --- a/src/main/java/com/destroystokyo/paper/entity/ai/VanillaGoal.java diff --git a/patches/api/0004-Bump-Dependencies.patch b/patches/api/0004-Bump-Dependencies.patch index 1f40e75e..58cf5a73 100644 --- a/patches/api/0004-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 31ef38465298b0bfc5c97810a8e3fb8ce8c466f3..389816e5264b1933bc48e23a512788a5629156bb 100644 +index 1f9ed00eb14b2c25bab7f067644eefb4306abab4..3577d12145ee01bbcadb49a5cfa7d937ec47a502 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,16 +25,16 @@ val annotationsVersion = "24.0.1" @@ -76,7 +76,7 @@ index 31ef38465298b0bfc5c97810a8e3fb8ce8c466f3..389816e5264b1933bc48e23a512788a5 } configure { -@@ -117,7 +120,7 @@ tasks.withType { +@@ -115,7 +118,7 @@ tasks.withType { options.use() options.isDocFilesSubDirs = true options.links( @@ -85,7 +85,7 @@ index 31ef38465298b0bfc5c97810a8e3fb8ce8c466f3..389816e5264b1933bc48e23a512788a5 "https://javadoc.io/doc/org.yaml/snakeyaml/2.0/", "https://javadoc.io/doc/org.jetbrains/annotations/$annotationsVersion/", // Paper - we don't want Java 5 annotations // Paper start -@@ -160,6 +163,9 @@ val scanJar = tasks.register("scanJarForBadCalls", io.papermc.paperweight.tasks. +@@ -158,6 +161,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/server/0008-Purpur-Base.patch b/patches/server/0008-Purpur-Base.patch new file mode 100644 index 00000000..fb8918be --- /dev/null +++ b/patches/server/0008-Purpur-Base.patch @@ -0,0 +1,100 @@ +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 + +Commit: 2e5dcc63d331fe121d18f37dcecc34be17bf7abb + +Original license: MIT +Original project: https://github.com/PurpurMC/Purpur + +diff --git a/build.gradle.kts b/build.gradle.kts +index 9fa1085f85f4ab29024c5efc0507ed86da18d461..f6ae0c843c6069ffedb1fb595b456e275cb81b4b 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.9.2") + runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.10") + runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.10") +@@ -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/0009-Purpur-Server-Changes.patch b/patches/server/0009-Purpur-Server-Changes.patch new file mode 100644 index 00000000..3142fc37 --- /dev/null +++ b/patches/server/0009-Purpur-Server-Changes.patch @@ -0,0 +1,24678 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Github Actions +Date: Fri, 16 Jun 2023 05:22:02 +0000 +Subject: [PATCH] Purpur Server Changes + +Commit: 2e5dcc63d331fe121d18f37dcecc34be17bf7abb + +Patches below are removed in this patch: +MC-238526-Fix-spawner-not-spawning-water-animals-cor.patch +Fix-cow-rotation-when-shearing-mooshroom.patch +Add-5-second-tps-average-in-tps.patch +Skip-events-if-there-s-no-listeners.patch +Fix-legacy-colors-in-console.patch +End-gateway-should-check-if-entity-can-use-portal.patch +Arrows-should-not-reset-despawn-counter.patch +Fix-outdated-server-showing-in-ping-before-server-fu.patch +Alternative-Keepalive-Handling.patch +Add-toggle-for-sand-duping-fix.patch +Logger-settings-suppressing-pointless-logs.patch +Dont-eat-blocks-in-non-ticking-chunks.patch +MC-121706-Fix-mobs-not-looking-up-and-down-when-stra.patch +Remove-Timings.patch +Remove-Mojang-Profiler.patch +MC-238526-Fix-spawner-not-spawning-water-animals-cor.patch +Fix-MC-123848.patch +Spark-Profiler.patch +Halloween-options-and-optimizations.patch + +Original license: MIT +Original project: https://github.com/PurpurMC/Purpur + +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/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/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 22884a2b148b9a5af8655bb754ebe73618218a83..5737fe8c23b46b37cea86524613dd3c32f4ca9cf 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -221,6 +221,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; + } +@@ -330,6 +345,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(Supplier feedbackSupplier, boolean broadcastToOps) { + boolean flag1 = this.source.acceptsSuccess() && !this.silent; + boolean flag2 = broadcastToOps && this.source.shouldInformAdmins() && !this.silent; +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index 4d7fada64d600d799ca6d9dbba314d87a5d44ffe..bf8b95c8d665605ad70c099aaa819e91561c3fea 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -223,6 +223,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) { +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 83cab746d1d6fe25c043c8aee28c39412b90c127..ec6b58dae525c81bbb1c0e2d96fbded6f00a45b5 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -48,6 +48,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 d0a8092bf57a29ab7c00ec0ddf52a9fdb2a33267..defe5951938ce3a7b7f83017d4af36bb49ff5be0 100644 +--- a/src/main/java/net/minecraft/core/Direction.java ++++ b/src/main/java/net/minecraft/core/Direction.java +@@ -238,6 +238,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); + } +@@ -350,6 +356,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 b4f5dbe9022dd20437c15c4f6fbe2ac06dacbadb..b52a9f05ada86f2d3767dd0d5ba8705e22f105d6 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; +@@ -1166,6 +1167,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 9b0049dfeaec9b688bf276f2ac2b18943b5696b2..d7563904232353cbf3b9255cedfb75920e35220c 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 f98545988a081564581a5ff43d2d9087b676b725..eb18c930831bc4f5650dbf2a949e99c8149eb41d 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -576,11 +576,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/game/ClientboundSetTimePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetTimePacket.java +index 9ec6145fe04ec64bbee8ec6a837719caebdbc6f5..358d610ad020cada1bb83e393deeeaaec05a2791 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetTimePacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetTimePacket.java +@@ -5,7 +5,7 @@ import net.minecraft.network.protocol.Packet; + + public class ClientboundSetTimePacket 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 c62a225a97deca88a15f3d7c61550debdcbfccfe..c21020ff14ea68d4638278aad0641a3119b2fdc8 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -278,6 +278,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public Commands vanillaCommandDispatcher; +@@ -287,11 +288,13 @@ 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 + + /* Drop global time updates + if (this.tickCount % 20 == 0) { +@@ -1638,7 +1664,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ return org.purpurmc.purpur.PurpurConfig.serverModName; // Purpur - Purpur > // Gale - branding changes - Gale > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! + } + + public SystemReport fillSystemReport(SystemReport details) { +@@ -2518,6 +2544,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + final io.papermc.paper.adventure.ChatDecorationProcessor processor = new io.papermc.paper.adventure.ChatDecorationProcessor(this, sender, commandSourceStack, message); +diff --git a/src/main/java/net/minecraft/server/PlayerAdvancements.java b/src/main/java/net/minecraft/server/PlayerAdvancements.java +index 8834a32bfb27653062d242144dcd75ce7e289abc..b71d9352115ff1d695bef8247a2b98285926b9ec 100644 +--- a/src/main/java/net/minecraft/server/PlayerAdvancements.java ++++ b/src/main/java/net/minecraft/server/PlayerAdvancements.java +@@ -250,6 +250,7 @@ public class PlayerAdvancements { + advancement.getRewards().grant(this.player); + // Paper start - Add Adventure message to PlayerAdvancementDoneEvent + if (message != null && this.player.level().getGameRules().getBoolean(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)) { ++ if (org.purpurmc.purpur.PurpurConfig.advancementOnlyBroadcastToAffectedPlayer) this.player.sendMessage(message); else // Purpur + this.playerList.broadcastSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message), false); + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +index 664cbce2e06fcb95d3d3d6c5302fc9119f938925..bc9778c705d23acd84fa1cdeff6b403b4cda3686 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 5cb15e2209d7b315904a1fc6d650ce1e75584271..7e21db60f3ace2a19686d6ea04b994ec3a793ec8 100644 +--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +@@ -45,6 +45,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 d601d287e94a59ff93b8a83a44dac02544d211df..0ff3b06a98b2f4514b2d861b92dd70fe678ae86c 100644 +--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java +@@ -59,6 +59,7 @@ public class GiveCommand { + boolean flag = entityplayer.getInventory().add(itemstack1); + ItemEntity entityitem; + ++ if (org.purpurmc.purpur.PurpurConfig.disableGiveCommandDrops) continue; // Purpur - add config option for toggling give command dropping + if (flag && itemstack1.isEmpty()) { + itemstack1.setCount(1); + entityitem = entityplayer.drop(itemstack1, 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 69c594bbd52335d6779b06f9273a1ef8e8138a67..035a886c550c8b08b5821277628b8b7bd7482866 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; +@@ -223,6 +224,15 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + io.papermc.paper.command.PaperCommands.registerCommands(this); + GaleCommands.registerCommands(this); // Gale - Gale commands - register commands + 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 +@@ -287,6 +297,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 +@@ -360,6 +394,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + if (LeafConfig.enableAsyncMobSpawning) mobSpawnExecutor.start(); // Pufferfish ++ org.purpurmc.purpur.task.BossBarTask.startAll(); // Purpur ++ org.purpurmc.purpur.task.BeehiveTask.instance().register(); // Purpur + return true; + } + } +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +index 37c647ec9eff5fb2729dc83efed7920bc0227f34..63df083f2f5a866e47d04989beb1b04da5f9a608 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("\\e\\[[\\d;]*[^\\d;]"); // CraftBukkit // Paper +- 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 780deea4805dfdd80d47e20ca1c2fcac17e5cabc..10b88fe8b2cb3f7fe920cad95254ef8d21bd1015 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -1004,7 +1004,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; +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 7ad872f8f769dfe6e3b6a0b8f83357076b33d22b..8244db99233dd69e3f5c4aa024a4e40b31b49d5a 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -70,7 +70,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 5d6c0f5d2d993ae3a044a1a02716a2662e9080d0..85219c6f64532b30904a2645aa37b59ae95841fe 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -213,6 +213,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 + private final RandomSequences randomSequences; + public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick + +@@ -222,6 +224,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); +@@ -663,7 +666,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 +@@ -733,6 +753,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 + } + + // Paper start +@@ -781,7 +802,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + 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()); +@@ -894,6 +915,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); + } + +@@ -902,8 +930,22 @@ 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(); + +@@ -945,10 +987,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 +@@ -1058,7 +1108,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); +@@ -1107,11 +1157,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)); + } + +@@ -1250,6 +1316,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.... +@@ -1257,6 +1324,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. +@@ -2738,7 +2806,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 00ce8b78e35055da01a053cb0d4ed914d50acebc..227368ac352e14f4d9f32d3847162cff13f5cf9b 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -278,6 +278,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 + + private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1)); + public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; +@@ -418,6 +423,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. +@@ -557,6 +563,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 +@@ -623,6 +632,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 +@@ -751,6 +763,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() { +@@ -989,6 +1010,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); +@@ -1090,14 +1112,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(); +@@ -1238,6 +1276,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.setServerLevel(worldserver); +@@ -1272,6 +1311,7 @@ public class ServerPlayer extends Player { + } + // Paper end + ++ this.spawnInvulnerableTime = worldserver.purpurConfig.playerSpawnInvulnerableTicks; // Purpur + return this; + } + } +@@ -1393,7 +1433,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); + } + } +@@ -1530,6 +1570,7 @@ public class ServerPlayer extends Player { + + @Override + public void openTextEdit(SignBlockEntity sign, boolean front) { ++ if (level().purpurConfig.signAllowColors) this.connection.send(sign.getTranslatedUpdatePacket(textFilteringEnabled, front)); // Purpur + this.connection.send(new ClientboundBlockUpdatePacket(this.level(), sign.getBlockPos())); + this.connection.send(new ClientboundOpenSignEditorPacket(sign.getBlockPos(), front)); + } +@@ -1770,6 +1811,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); +@@ -2075,6 +2136,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)); + } + +@@ -2089,8 +2151,68 @@ 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()) { ++ String playerName = this.getGameProfile().getName(); ++ if (org.purpurmc.purpur.PurpurConfig.afkBroadcastUseDisplayName) { ++ net.kyori.adventure.text.Component playerDisplayNameComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(this.getBukkitEntity().getDisplayName()); ++ playerName = net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(playerDisplayNameComponent); ++ } ++ server.getPlayerList().broadcastMiniMessage(String.format(msg, playerName), 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; + } +@@ -2602,8 +2724,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() { +@@ -2652,4 +2782,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 d65b9d2d9070d8dee2c144c9b37d6605a59cfafe..642341564cabb550503b3ef6f31117895ba86cba 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -398,6 +398,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 + +@@ -428,7 +429,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(); +@@ -517,6 +518,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; +@@ -577,7 +579,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()) { +@@ -613,4 +615,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/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 8d90209a01020d44626f56e2cb0dd5eca300f699..0b3c784f6138db19594b443073430a9ec0dda052 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -347,6 +347,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(); + } +@@ -460,6 +474,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() * 1000L * 60L && !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 + } +@@ -778,6 +798,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(); +@@ -854,6 +876,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; + } + +@@ -1258,10 +1281,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; + } +@@ -1285,6 +1310,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; + } +@@ -1338,13 +1364,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)); +@@ -1356,10 +1385,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); +@@ -1369,11 +1401,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 + } + } + +@@ -1386,6 +1418,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.serverLevel()); +@@ -1415,8 +1457,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + @Override + public void handleMovePlayer(ServerboundMovePlayerPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); +- 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.serverLevel(); + +@@ -1581,7 +1631,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + if (!this.player.isChangingDimension() && d10 > 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 + } + + // Paper start - optimise out extra getCubes +@@ -1632,6 +1682,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(); +@@ -1670,6 +1722,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(); +@@ -1702,6 +1761,13 @@ 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, double newX, double newY, double newZ) { + AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); + Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); +@@ -2047,6 +2113,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 { +@@ -2100,12 +2167,21 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + @Override + public void handleResourcePackResponse(ServerboundResourcePackPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); ++ // 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 +@@ -2410,7 +2486,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)); + +@@ -2836,6 +2912,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + AABB axisalignedbb = entity.getBoundingBox(); + + if (axisalignedbb.distanceToSqr(this.player.getEyePosition()) < ServerGamePacketListenerImpl.getMaxInteractionDistanceSquared(this.player.level())) { // Gale - make max interaction distance configurable ++ 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); +@@ -2849,6 +2926,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. +@@ -3401,6 +3480,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(); +@@ -3556,6 +3641,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) { +@@ -3580,6 +3666,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 5b7c12db86be64433c65e31e3ecc0b444b0ddf48..5ed89ce9d2c29927f48c1f7f8f9288f39d68b56c 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -225,6 +225,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); + +@@ -346,7 +348,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/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 697bd3a7c0ab1e941355a818bffe85bdb28a70db..5ecf6b85a909ea8934f50e0dbe262a4f409a855f 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -492,6 +492,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 + if (GaleGlobalConfiguration.get().logToConsole.playerLoginLocations) { // Gale - JettPack - make logging login location configurable + PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ()); +@@ -607,6 +608,7 @@ 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.serverLevel(); + + entityplayer.awardStat(Stats.LEAVE_GAME); +@@ -761,7 +763,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 + } + } +@@ -1001,6 +1003,8 @@ public abstract class PlayerList { + } + // Paper end + ++ entityplayer1.spawnInvulnerableTime = entityplayer1.level().purpurConfig.playerSpawnInvulnerableTicks; // Purpur ++ + // CraftBukkit end + return entityplayer1; + } +@@ -1100,6 +1104,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(); + +@@ -1203,6 +1221,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)); + } +@@ -1211,6 +1230,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) { +diff --git a/src/main/java/net/minecraft/server/players/SleepStatus.java b/src/main/java/net/minecraft/server/players/SleepStatus.java +index 823efad652d8ff9e96b99375b102fef6f017716e..caa8a69bde0c212c36dd990a67836ac2f95548c0 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/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 25a5a3b949a0eb632611355e74ccd4865be108ca..14fcfd7c1d3a62833978e163f4e0d6f9b2203042 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 bcce17f884b57e619749351b7b2047f0a5f9be71..878f3da0067f89f47141f41c733bef67f0987929 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 7652900f88fe778c3855536e8744445704a1f8b1..8c1cf9f56a13cae026a18d14b8ac3b884f7d86ca 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -157,7 +157,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 +@@ -324,7 +324,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; +@@ -366,7 +366,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; +@@ -407,6 +407,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(); +@@ -482,6 +483,41 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return false; + } + ++ public boolean canSaveToDisk() { ++ return true; ++ } ++ ++ // Gale start - JettPack - optimize sun burn tick - cache eye blockpos ++ private BlockPos cached_eye_blockpos; ++ private int cached_position_hashcode; ++ // Gale end - JettPack - optimize sun burn tick - cache eye blockpos ++ ++ // Purpur start - copied from Mob ++ public boolean isSunBurnTick() { ++ if (this.level().isDay() && !this.level().isClientSide) { ++ // Gale start - JettPack - optimize sun burn tick - optimizations and cache eye blockpos ++ int positionHashCode = this.position.hashCode(); ++ if (this.cached_position_hashcode != positionHashCode) { ++ this.cached_eye_blockpos = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); ++ this.cached_position_hashcode = positionHashCode; ++ } ++ ++ float f = this.getLightLevelDependentMagicValue(cached_eye_blockpos); // Pass BlockPos to getBrightness ++ ++ // Check brightness first ++ if (f <= 0.5F) return false; ++ if (this.random.nextFloat() * 30.0F >= (f - 0.4F) * 2.0F) return false; ++ // Gale end - JettPack - optimize sun burn tick - optimizations and cache eye blockpos ++ boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; ++ ++ if (!flag && this.level().canSeeSky(this.cached_eye_blockpos)) { // Gale - JettPack - optimize sun burn tick - optimizations and cache eye blockpos ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ + public final boolean hardCollides() { + return this.hardCollides; + } +@@ -587,7 +623,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(); +@@ -908,10 +944,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + public void checkBelowWorld() { + // 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(io.papermc.paper.util.MCUtil.toLocation(this.level, this.level.getSharedSpawnPos())); else // Purpur + this.onBelowWorld(); + } + +@@ -1803,7 +1840,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) { +@@ -1872,7 +1909,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) { +@@ -2481,6 +2518,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"); +@@ -2649,6 +2691,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"); +@@ -2966,6 +3013,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 +@@ -3007,6 +3061,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 { +@@ -3066,12 +3128,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; +@@ -3120,7 +3185,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 + } + } + +@@ -3306,7 +3371,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() { +@@ -3765,7 +3830,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) { +@@ -4072,6 +4137,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) {} + +@@ -4353,6 +4432,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()) { // Gale - Airplane - reduce entity fluid lookups if no fluids - cost of a lookup here is the same cost as below, so skip + return false; +@@ -4899,4 +4984,45 @@ 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; ++ } ++ // 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 3ff999734d14e2b6e7828e117f5ee32a60c26bc1..cfa9607241c3e69777ffc317206996c2f783437a 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 aa5cec6d56d7a8e80861aa4c9b4a74ca3e64be8c..a8ffa403ac48041e17aa4ce057be0539e5468fd4 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 eca634792d2a7cc649675e3394e84dbaf1453905..724bf857bf1b89cb0947b8a82e0ce09a0bec0335 100644 +--- a/src/main/java/net/minecraft/world/entity/ExperienceOrb.java ++++ b/src/main/java/net/minecraft/world/entity/ExperienceOrb.java +@@ -313,7 +313,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); + +@@ -331,7 +331,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(); +@@ -359,13 +359,15 @@ public class ExperienceOrb extends Entity { + } + } + ++ // Purpur start + public int durabilityToXp(int repairAmount) { +- return repairAmount / 2; ++ return (int) (repairAmount / (2 * level().purpurConfig.mendingMultiplier)); + } + + public int xpToDurability(int experienceAmount) { +- return experienceAmount * 2; ++ return (int) ((experienceAmount * 2) * level().purpurConfig.mendingMultiplier); + } ++ // Purpur end + + public int getValue() { + return this.value; +diff --git a/src/main/java/net/minecraft/world/entity/GlowSquid.java b/src/main/java/net/minecraft/world/entity/GlowSquid.java +index 759713f7c646aaf1a918c87a2834a1d405385dad..83660fad1e4c0ce5800f6e62f915ccd87b9d04ac 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 07ab60405dfbef46e74911a750ab0a9d067111c9..b65a3a09e96a7656d6372ecf5d7ba73e877121ec 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -219,9 +219,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; +@@ -254,6 +254,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; +@@ -263,6 +264,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() { +@@ -287,7 +289,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()); +@@ -303,6 +306,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; + } +@@ -338,6 +343,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) { +@@ -350,7 +356,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.tryAddSoulSpeed(); + } + +- if (!this.level().isClientSide && this.fallDistance > 3.0F && onGround && !state.isAir()) { ++ if (!this.level().isClientSide && this.fallDistance > this.safeFallDistance && onGround && !state.isAir()) { // Purpur + double d1 = this.getX(); + double d2 = this.getY(); + double d3 = this.getZ(); +@@ -365,7 +371,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + d3 = (double) landedPosition.getZ() + 0.5D + d5 / d6 * 0.5D; + } + +- float f = (float) Mth.ceil(this.fallDistance - 3.0F); ++ float f = (float) Mth.ceil(this.fallDistance - this.safeFallDistance); // Purpur + double d7 = Math.min((double) (0.2F + f / 15.0F), 2.5D); + int i = (int) (150.0D * d7); + +@@ -425,6 +431,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) { serverPlayer.teleport(io.papermc.paper.util.MCUtil.toLocation(level(), ((ServerLevel) level()).getSharedSpawnPos())); return; } // Purpur + this.hurt(this.damageSources().outOfBorder(), (float) Math.max(1, Mth.floor(-d0 * d1))); + } + } +@@ -436,7 +443,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(); + +@@ -448,7 +455,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 + } + } + +@@ -798,6 +805,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 +@@ -882,6 +890,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 +@@ -1027,9 +1040,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; +@@ -1089,6 +1124,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; +@@ -1476,13 +1512,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) { +@@ -1593,6 +1629,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); +@@ -1759,7 +1807,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(); + +@@ -1805,6 +1853,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; +@@ -1813,6 +1862,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, () -> { +@@ -2083,7 +2133,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 + } + } + +@@ -2306,6 +2356,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. +@@ -2522,7 +2586,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + @Override + protected void onBelowWorld() { +- this.hurt(this.damageSources().fellOutOfWorld(), 4.0F); ++ this.hurt(this.damageSources().fellOutOfWorld(), (float) level().purpurConfig.voidDamageDealt); // Purpur + } + + protected void updateSwingTime() { +@@ -2719,7 +2783,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + protected long lastJumpTime = 0L; // Paper +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3d = this.getDeltaMovement(); + // Paper start + long time = System.nanoTime(); +@@ -2871,6 +2935,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); + } + } +@@ -3475,8 +3540,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + + this.pushEntities(); + // Paper start +- if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { +- if (this.xo != this.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()); +@@ -3486,12 +3553,48 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.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() { +@@ -3512,7 +3615,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 1674f9accbbbb9ecdd99f05da6032398c4d82b38..29fc0a756c4541bb5292c74edef983c4fad373f1 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) { +@@ -148,8 +150,8 @@ public abstract class Mob extends LivingEntity implements Targeting { + this.goalSelector = new GoalSelector(); + this.targetSelector = new GoalSelector(); + // Gale end - Purpur - remove vanilla profiler +- 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); +@@ -325,6 +327,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 +@@ -369,8 +372,28 @@ public abstract class Mob extends LivingEntity implements Targeting { + this.resetAmbientSoundTime(); + this.playAmbientSound(); + } ++ incrementTicksSinceLastInteraction(); // 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(); +@@ -560,6 +583,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + } + + nbt.putBoolean("Bukkit.Aware", this.aware); // CraftBukkit ++ nbt.putInt("Purpur.ticksSinceLastInteraction", this.ticksSinceLastInteraction); // Purpur + } + + @Override +@@ -630,6 +654,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 +@@ -673,7 +702,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + @Override + public void aiStep() { + super.aiStep(); +- if (!this.level().isClientSide && this.canPickUpLoot() && this.isAlive() && !this.dead && this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { ++ 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(); +@@ -1143,6 +1172,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) { +@@ -1237,7 +1272,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); +@@ -1285,6 +1320,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); +@@ -1358,7 +1394,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() { +@@ -1663,6 +1699,7 @@ public abstract class Mob extends LivingEntity implements Targeting { + this.setLastHurtMob(target); + } + ++ if (target instanceof ServerPlayer) this.ticksSinceLastInteraction = 0; // Purpur + return flag; + } + +@@ -1678,34 +1715,8 @@ public abstract class Mob extends LivingEntity implements Targeting { + + } + +- // Gale start - JettPack - optimize sun burn tick - cache eye blockpos +- private BlockPos cached_eye_blockpos; +- private int cached_position_hashcode; +- // Gale end - JettPack - optimize sun burn tick - cache eye blockpos +- + public boolean isSunBurnTick() { +- if (this.level().isDay() && !this.level().isClientSide) { +- // Gale start - JettPack - optimize sun burn tick - optimizations and cache eye blockpos +- int positionHashCode = this.position.hashCode(); +- if (this.cached_position_hashcode != positionHashCode) { +- this.cached_eye_blockpos = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); +- this.cached_position_hashcode = positionHashCode; +- } +- +- float f = this.getLightLevelDependentMagicValue(cached_eye_blockpos); // Pass BlockPos to getBrightness +- +- // Check brightness first +- if (f <= 0.5F) return false; +- if (this.random.nextFloat() * 30.0F >= (f - 0.4F) * 2.0F) return false; +- // Gale end - JettPack - optimize sun burn tick - optimizations and cache eye blockpos +- boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; +- +- if (!flag && this.level().canSeeSky(this.cached_eye_blockpos)) { // Gale - JettPack - optimize sun burn tick - optimizations and cache eye blockpos +- return true; +- } +- } +- +- return false; ++ return super.isSunBurnTick(); // Purpur + } + + @Override +@@ -1752,4 +1763,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 c6f4e9e465e24a37b773b348feb24feb0ac3adf7..00790f8c0c7ba5132b51cdaa58e4d3419aac23ab 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 +@@ -27,14 +27,22 @@ public class AttributeMap { + // Gale end - Lithium - replace AI attributes with optimized collections + private final AttributeSupplier supplier; + private final java.util.function.Function createInstance; // Gale - Airplane - reduce entity allocations ++ 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); // Gale - Airplane - reduce entity allocations + } + + private void onAttributeModified(AttributeInstance instance) { +- if (instance.getAttribute().isClientSyncable()) { ++ if (instance.getAttribute().isClientSyncable() && (entity == null || entity.shouldSendAttribute(instance.getAttribute()))) { // Purpur + this.dirtyAttributes.add(instance); + } + +@@ -46,7 +54,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/HarvestFarmland.java b/src/main/java/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java +index 5bb3db5e1f47ef56ef40c84f06a1c5ae59f84c89..5a807bce1db30cfc753caa588cd0ef632881d0dd 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 +@@ -39,17 +39,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(); +@@ -80,6 +82,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; + } + +@@ -105,7 +108,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); +@@ -113,14 +116,14 @@ 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) { + ItemStack itemstack = inventorysubcontainer.getItem(j); + boolean flag = false; + +- if (!itemstack.isEmpty() && itemstack.is(ItemTags.VILLAGER_PLANTABLE_SEEDS)) { ++ if (!itemstack.isEmpty() && (itemstack.is(ItemTags.VILLAGER_PLANTABLE_SEEDS) || this.clericWartFarmer && itemstack.getItem() == net.minecraft.world.item.Items.NETHER_WART)) { + Item item = itemstack.getItem(); + + if (item instanceof BlockItem) { +@@ -138,7 +141,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 42ae4d293a420f0b8eb476df6389b2e7a693895f..97c20c5b89e6d7e4ed844eff39ee55dfa8988d37 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 050be72c815010bf3f4b72427e2052b00420e8ee..8a213205f57c8dcd2226d7194d74b1b13dec5a78 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 94fb9bcf601832ee934331c0376de8707b5043c5..e49e6b9b7b0f1fab6a8888fcfd67d709d5a0dbd7 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 +@@ -3,6 +3,8 @@ package net.minecraft.world.entity.ai.behavior; + import com.google.common.collect.ImmutableMap; + + import java.util.Arrays; ++ ++import com.google.common.collect.ImmutableSet; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.world.SimpleContainer; + import net.minecraft.world.entity.EntityType; +@@ -60,6 +62,12 @@ public class TradeWithVillager extends Behavior { + throwHalfStack(entity, WHEAT_SINGLETON_ARRAY, villager); // Gale - optimize villager data storage + } + ++ // 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).toArray(new Item[0]), villager); ++ } ++ // Purpur end ++ + // Gale start - optimize villager data storage + if (this.trades != null && 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 03d77c6d4697d4934ca65e3329982f0efe089820..ce7e3e90993b76225dc820a21e04267e488f5f7b 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..384bed4505b6cabb1ae151cd2c4eb5e56f24563f 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) -> { +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 4253b3b1263a7ae5a2f5f3a34674dfea615a81ea..a987c94fd321f51241c405659d6a0b2327a612ae 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 0fe194854a60a8d09f2a793a34132f55cb338713..ea859a491e238c221e7c9721ab03392cb8d3589c 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 +@@ -75,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); + } + +@@ -85,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/LlamaFollowCaravanGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +index 21725aee29e9120d1c7e1e19f91c21a73a28844f..3fc9528201fb96d6a0f905afe0b6a82ec88a7235 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/RemoveBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +index 0035461aff86fa3f44c860e7d77589b974446048..8c08457b5a9ade9b602851c9a12b015e1b4b5a36 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 b463fdb812f1c402885f1538e1c213aea1374e5c..952b244a7423d44a0e755764e4b4b5a83ee7f42b 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..7b99c3446b50939241d3e220d93e05649f72a6df 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.level().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 0d9b194781d152e842c9a4b8d6f23d307b2e4452..00cf59524477ec79d4354cc403fc3e75a63b81a0 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/sensing/SecondaryPoiSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +index 53b0519bbc5d52490040eaf0fe449648f021d0c2..b323157327203f5614d0051284b6bb247a5a564d 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 +@@ -31,6 +31,13 @@ public class SecondaryPoiSensor extends Sensor { + return; + } + // Gale end - Lithium - skip secondary POI sensor if absent ++ // 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(); + @Nullable ArrayList list = null; // Gale - optimize villager data storage +@@ -52,7 +59,7 @@ public class SecondaryPoiSensor extends Sensor { + } + } + +- Brain brain = entity.getBrain(); ++ //Brain brain = entity.getBrain(); // Purpur - moved up + // Gale start - optimize villager data storage + if (list != null) { + list.trimToSize(); +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 57bd0b2d01c92f08cc41db59b38641a82653af0e..ffea5cd106bc77236c9c495b37f714d798270ead 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 233ea1d95cb3e6e002dc2cd801feaed38dfc3fbb..6338f37bdcce7fbbe0002367fd80cb0c0b801d62 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,59 @@ 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)); ++ } ++ } ++ // Purpur end ++ + @Override + public boolean isFlapping() { + return !this.isResting() && this.tickCount % Bat.TICKS_PER_FLAP == 0; +@@ -96,7 +144,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 +176,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(); +@@ -222,6 +278,28 @@ public class Bat extends AmbientCreature { + } + } + ++ @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; ++ } ++ + @Override + public void readAdditionalSaveData(CompoundTag nbt) { + super.readAdditionalSaveData(nbt); +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 2249fc6dd98afb8d52623b5864955fdd3b3fc042..2ccfaab0a02cf5ff9779e250fb79a75a9852e10d 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 1430f598073aab7387cf5985a7b3a84d3f53548c..15feb1baed21b2ed3fb90d68aa9b2b97aa664b15 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Animal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Animal.java +@@ -40,6 +40,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); +@@ -151,7 +152,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; +@@ -233,12 +234,20 @@ public abstract class Animal extends AgeableMob { + AgeableMob entityageable = this.getBreedOffspring(world, other); + + if (entityageable != null) { +- entityageable.setBaby(true); +- entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F); +- // CraftBukkit start - call EntityBreedEvent ++ // Purpur start + ServerPlayer breeder = Optional.ofNullable(this.getLoveCause()).or(() -> { + return Optional.ofNullable(other.getLoveCause()); + }).orElse(null); ++ if (breeder != null && world.purpurConfig.animalBreedingCooldownSeconds > 0) { ++ if (world.hasBreedingCooldown(breeder.getUUID(), this.getClass())) { ++ return; ++ } ++ world.addBreedingCooldown(breeder.getUUID(), this.getClass()); ++ } ++ entityageable.setBaby(true); ++ entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F); ++ // CraftBukkit start - call EntityBreedEvent ++ // Purpur end + int experience = this.getRandom().nextInt(7) + 1; + org.bukkit.event.entity.EntityBreedEvent entityBreedEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreedEvent(entityageable, this, other, breeder, this.breedItem, experience); + if (entityBreedEvent.isCancelled()) { +@@ -266,8 +275,10 @@ public abstract class Animal extends AgeableMob { + entityplayer.awardStat(Stats.ANIMALS_BRED); + CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer, this, entityanimal, entityageable); + } // Paper +- this.setAge(6000); +- entityanimal.setAge(6000); ++ // Purpur start ++ this.setAge(this.getPurpurBreedTime()); ++ entityanimal.setAge(entityanimal.getPurpurBreedTime()); ++ // Purpur end + this.resetLove(); + entityanimal.resetLove(); + worldserver.broadcastEntityEvent(this, (byte) 18); +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 f6062bf8c888baeb7b421150a2c64bf1af1a312b..2677e7899aa98fc04070f5bbbc40da6117d89efa 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; +@@ -142,6 +143,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) { +@@ -150,22 +152,69 @@ 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)); ++ } ++ } ++ // Purpur end ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -180,6 +229,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)); +@@ -195,6 +245,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)); +@@ -343,7 +394,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 { +@@ -383,6 +434,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) { +@@ -415,6 +467,26 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + } + } + ++ @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; ++ } ++ + @Override + public int getRemainingPersistentAngerTime() { + return (Integer) this.entityData.get(Bee.DATA_REMAINING_ANGER_TIME); +@@ -731,6 +803,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); +@@ -787,6 +860,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 +@@ -833,6 +907,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; +@@ -877,16 +952,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 90ce201bc7c47cef9bc59d7b535a7453854bac75..3347b39fa1bc3308aa3b70b4169523885b91a047 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); ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + 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 84cefbc339ca368ac306d521012ce2f826a660b5..00bdb2b75221349cb40da2b5fc1563393fd17ef6 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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..f0b6118a9995bb41836685bbf94d2e7fb15761eb 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 4b63f6aa3d19cc4f47b05d531df3a43bf398c9ea..6aeb3a6ac9665a0c4b929d0f034f177df88109e0 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 501e6cb4aa83f81c1f657e41f4e7f11d19d46831..dcb23409b8492200771c515a83d086b39777972f 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 boolean isNaturallyAggressiveToPlayers; // Purpur ++ private int spitCooldown; // 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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(1, new MeleeAttackGoal(this, 1.2000000476837158D, true)); // Purpur ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // 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 844083101e9763330af0175388f6cdda27a97ecc..d9d4341c4511f4982f691eeea80ef17de759291a 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); ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 f383928fc5b331ddf128bdcb6a23010d8fe088d3..64aba511e615983988cdb6a0fd45b7d9d4f2f16d 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java +@@ -60,14 +60,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); ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @Override + protected void registerGoals() { ++ if (level().purpurConfig.ironGolemCanSwim) this.goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.FloatGoal(this)); // Purpur ++ if (this.level().purpurConfig.ironGolemPoppyCalm) this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.ReceiveFlower(this)); // Purpur ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(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)); +@@ -75,6 +120,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)); +@@ -139,6 +185,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); + } + +@@ -146,6 +193,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); + } + +@@ -270,13 +318,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; + +@@ -285,6 +333,7 @@ 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 e3b92e8b2274ee6d07d1e9c74f669aeaab594919..d1fdf8a13a85fa9331e3d4fdfa3e55fd85c12bc0 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 9b807b318a2843810c3963277d464439a865cfb6..92638ac85237e4532024fbf75c7d5d38dcd8b4bc 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); ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 3cc7af656433117991547476c118b58cff95e8e2..8526556c42a411414b322553c295fb430ceffa71 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)); ++ } ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 a197337f2e09f53cf382022569c8836745d78769..54f5206b686c3cf4d2e5b470c07047a518f5dd00 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 01a58fe71669bf199511c906363d036ed880aa91..2bd8f6d05728b48e184f4835d6acb3a00cf66153 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + // 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,14 @@ public class Rabbit extends Animal implements VariantHolder { + + @Override + protected float getJumpPower() { ++ // Purpur start ++ if (getRider() != null && this.isControllable()) { ++ if (getForwardMot() < 0) { ++ setSpeed(getForwardMot() * 2F); ++ } ++ return actualJump ? 0.5F : 0.3F; ++ } ++ // Purpur end + float f = 0.3F; + + if (this.horizontalCollision || this.moveControl.hasWanted() && this.moveControl.getWantedY() > this.getY() + 0.5D) { +@@ -138,7 +213,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(); + +@@ -188,6 +263,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; + } +@@ -405,10 +487,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); + +@@ -472,7 +567,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; +@@ -483,14 +578,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 +@@ -552,7 +647,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..382e47f26ee94506cb76463a677351b9bdcf8040 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 d8f99e1221609d481ee79ee31f645731f34e1021..06c68e64031edfa4064ba570d4bd5f58cd0caa52 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Sheep.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Sheep.java +@@ -117,10 +117,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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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)); +@@ -255,7 +293,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); +@@ -270,14 +308,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 96bfba9d3ccdbf95b8eea4038bf42e6b1430181d..7cd368b252fa6ad6956685f68c730c4a2273c10e 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 b72006c4b2342ca9d9a81f54f89fa6d979c33c85..41797940d89fec55cb7de4c63eb3ea5cdb4be967 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); ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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..87b6f6b10ba6e3d9c6a42298a2019a526a183d90 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + 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 098ae9d8fa3e7cad8473a877decba771f6bd1b36..7787b121def6525642672dfd67fae9ac0e010f52 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + 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/Wolf.java b/src/main/java/net/minecraft/world/entity/animal/Wolf.java +index 27c4dd3605373f08078048fe923a8f6f4d3ccf3b..e7558511e0690d80ff444e71b7524d564d68842b 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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; +@@ -418,7 +548,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); +@@ -429,6 +559,19 @@ 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 + } else { + 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 047780d1cdbe3f3ffb5f03d03733fb486f28cf98..4f47a6945e2836e692033b18e56ced9b0d2b51af 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, VibrationS + 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()); + this.vibrationUser = new Allay.VibrationUser(); + this.vibrationData = new VibrationSystem.Data(); +@@ -118,6 +131,28 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS + } + // 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); +@@ -225,7 +260,7 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- 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); + AllayAi.updateActivity(this); + super.customServerAiStep(); +@@ -368,9 +403,31 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS + + @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 e54e873169a822844b87adc6c4703974bf89e379..3ded7244c023a7a1963a497792dd168da6757b27 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; +@@ -517,14 +554,22 @@ public class Axolotl extends Animal implements LerpingModel, 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(); ++ } ++ // Purpur end ++ ++ public int getPurpurBreedTime() { ++ return this.level().purpurConfig.frogBreedingTicks; ++ } ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); +@@ -343,7 +396,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 c83dabddf93249a6477c10725622119c939db4d5..e91b4d63d42276f8a498cab7c439c785730f3f6f 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,7 +117,7 @@ public class Tadpole extends AbstractFish { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- 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); + TadpoleAi.updateActivity(this); + 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 c88c2312509906dbf45118b4a82fed2fc3e0d14d..6e31ce1cc293c2a92af15dd74e51731196108dcd 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(Goat.MEMORY_TYPES, Goat.SENSOR_TYPES); +@@ -191,7 +223,7 @@ public class Goat extends Animal { + private int behaviorTick = 0; // Pufferfish + @Override + protected void customServerAiStep() { +- 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); + GoatAi.updateActivity(this); + super.customServerAiStep(); +@@ -385,6 +417,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 79b6e241f425622fdc575b77d8dce7061c0ab783..8bf1105601f000142246554d177965d1dc6a3339 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 ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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(); + } +@@ -332,7 +381,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() { +@@ -1253,7 +1302,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..72ad12175325091397459e06743875cce6df8d94 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 5f5dc651d570989ec1294c31a14dcfede466b80a..3b1faa63e46a48e83ea672cf6da444a1d7e13270 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 3bbb1455773570e3f7f6b8b144d3c589e2705b81..70ac8c2fef15587d57f37c72db7913fd89f05d31 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,9 +73,84 @@ 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); ++ } ++ // Purpur end ++ ++ @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; + } + + public boolean isTraderLlama() { +@@ -110,7 +185,7 @@ public class Llama extends AbstractChestedHorse implements VariantHolder public + super.jumpFromGround(); + double d0 = this.moveControl.getSpeedModifier(); + +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 a6f3fba3b02b0b4d2a4e9e5205301c6f52d0188a..4eebfc27ded55e4d764d04f35d3e9c9e0791c89f 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 5465711d486e5f265a26042031e895fb09e30608..77ffceba328bb1d87ef755dd82b50d02ee120d4d 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); +@@ -42,6 +48,22 @@ public class EndCrystal extends Entity { + this.setPos(x, y, z); + } + ++ 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; ++ } ++ + @Override + protected Entity.MovementEmission getMovementEmission() { + return Entity.MovementEmission.NONE; +@@ -77,9 +99,53 @@ 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; + } ++ // 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 92666c48620078623a451fbf68f673cb9f81c4b5..cf6330694b811cef743763e0915824df8f75fe27 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 +@@ -105,9 +105,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.fightOrigin = BlockPos.ZERO; + this.growlTime = 100; + this.nodes = new Node[24]; +@@ -126,7 +128,37 @@ public class EnderDragon extends Mob implements Enemy { + this.noPhysics = true; + this.noCulling = true; + 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; + } + + public void setDragonFight(EndDragonFight fight) { +@@ -141,6 +173,27 @@ public class EnderDragon extends Mob implements Enemy { + return this.fightOrigin; + } + ++ @Override ++ public boolean isControllable() { ++ return level().purpurConfig.enderDragonControllable; ++ } ++ ++ @Override ++ public double getMaxY() { ++ return level().purpurConfig.enderDragonMaxY; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.enderDragonMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.enderDragonTakeDamageFromWater; ++ } ++ + public static AttributeSupplier.Builder createAttributes() { + return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 200.0D); + } +@@ -202,6 +255,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()); +@@ -228,6 +312,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; +@@ -240,9 +326,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; +@@ -287,7 +373,7 @@ public class EnderDragon extends Mob implements Enemy { + } + + this.phaseManager.getCurrentPhase().doClientTick(); +- } else { ++ } else if (!hasRider) { // Purpur + DragonPhaseInstance idragoncontroller = this.phaseManager.getCurrentPhase(); + + idragoncontroller.doServerTick(); +@@ -356,7 +442,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)); +@@ -400,7 +486,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); + } +@@ -532,7 +618,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; +@@ -667,7 +753,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; + } + +@@ -1102,6 +1188,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 f69e0b11be74ac83694f59999b3f07a318410c19..f4596e90d2a569eb1118455b2eeec2036d806533 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,20 +84,59 @@ 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); ++ @Nullable private java.util.UUID summoner; // Purpur ++ private int shootCooldown = 0; // 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; + } + ++ @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; ++ } ++ + @Override + protected PathNavigation createNavigation(Level world) { + FlyingPathNavigation navigationflying = new FlyingPathNavigation(this, world); +@@ -108,13 +147,124 @@ 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); ++ } ++ }; ++ skull.setPosRaw(headX, headY, headZ); ++ level().addFreshEntity(skull); ++ } ++ ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ // 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.level().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 8a8b9e8983be1acad66ad875c901be5bbdeabb1f..8ba5701ebec928ba52d88a6671332c80e07f384c 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) { +@@ -603,7 +606,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()); + } + +@@ -679,6 +682,7 @@ public class ArmorStand extends LivingEntity { + + @Override + public void tick() { ++ maxUpStep = level().purpurConfig.armorstandStepHeight; + // Paper start + if (!this.canTick) { + if (this.noTickPoseDirty) { +@@ -1005,4 +1009,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 4fca705a68422e3829d82e024cbadfb077710a6d..6a313d02db33621b286531e316479145d884f173 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()) { +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 0f4ef103afcbabc04880c8fc3547b861341c15fc..4b3a8e4a084585d56dd10a08405463b1a677368b 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); +@@ -381,6 +387,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 { +@@ -579,6 +594,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 = this.level().paperConfig().entities.spawning.altItemDespawnRate.enabled ? this.level().paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), this.level().spigotConfig.itemDespawnRate) : this.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 9ca1e9d95e62929c0015d5ca2c2f9c70e421842e..d96482dc1df0f9076951c745cc001e2eb300011a 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(); + } + +@@ -205,7 +187,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 +218,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 +227,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 17aa7676ab624440651850bbe5689f8a6c9dbeed..a8b58469fd8a1ed4ec0ce443cf05557903527bd7 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)); ++ } ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.blazeMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.blazeAlwaysDropExp; ++ } ++ + @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 da48bd8e15463be8170262ae90a8e95575959bf2..bf218586a79ea5f4288a64ccf6c245ee8491ce9a 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 71587510ef1b6cbd95cb55556cd93c6c194ea5f5..05576ef4d7476f1c04d47e76bec277abea3f6afb 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,99 @@ 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 ++ } ++ // 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])); + } +@@ -173,6 +251,37 @@ public class Creeper extends Monster implements PowerableMob { + } + } + ++ @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; ++ } ++ + @Override + protected SoundEvent getHurtSound(DamageSource source) { + return SoundEvents.CREEPER_HURT; +@@ -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 991d728db2a3b64316fc2102cf3aee470327a62e..63a1cf5604c14025171d7be7434e2d6b64c98107 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; ++ } ++ // Purpur end ++ ++ @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 isSensitiveToWater() { ++ return this.level().purpurConfig.drownedTakeDamageFromWater; ++ } ++ ++ @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 ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.drownedAlwaysDropExp; ++ } ++ + @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 (this.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 8f481e11815d7162dd62a2b850b3d2af6d904519..b30f13d2a7198f568bc36c0d974fd6dc6be292c2 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + 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 f21d65106f078799e443e1a5a998a5d6b4c4fcb2..ad1d903337ec096640236c77255a79174558bc21 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; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.endermanMaxHealth); ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.endermanAlwaysDropExp; + } + + @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 29b8771ec85f07fcd9d9ba041c9a25a212009b16..e131949bd6aab4c8ff5cd4ea57280ad945ceef67 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 a6dde4e31790eaecff27135a4036627192c85702..4a87b76cc92220f6569b976871a86c8eab3c218d 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 592b0dae251800552a0771ec46b4b8532b63075d..b0aad5778a12d3f0f2ef806f856064c4da842fb3 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,47 @@ 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)); ++ } ++ } ++ // 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; + })); +@@ -96,6 +132,21 @@ public class Ghast extends FlyingMob implements Enemy { + } + } + ++ @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; ++ } ++ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); +@@ -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..88b0dffe2c2b4da1406f218186d28f018ee879a3 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Giant.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Giant.java +@@ -1,17 +1,122 @@ + 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 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 boolean isSensitiveToWater() { ++ return this.level().purpurConfig.giantTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.giantAlwaysDropExp; ++ } ++ // Purpur end ++ ++ @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 ++ 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 +@@ -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 5e9b9e9f124ec6103b2e832ae94bc5b334a2c533..96338ed7952683f56d9a406d0956ce230581f146 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 efe8e6b895da1a81e5de42697fda58443cf15a59..cd78fbca21112a2354bc40747531e5202bbd5f20 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Husk.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Husk.java +@@ -20,6 +20,59 @@ 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; ++ } ++ // Purpur end ++ ++ @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; + } + + public static boolean checkHuskSpawnRules(EntityType type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, RandomSource random) { +@@ -28,7 +81,7 @@ public class Husk extends Zombie { + + @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 63fce7e3d9f59f36e29bc827a46396d73143bb8b..f9cddf46d1dbcabc738842ba039daa76bb6f3eb5 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 2858ea5562d06c11e5c7337a2b123f9be7a3f62e..1ad97267394d3717b1871336193cdc91f3ffb276 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 ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes().add(Attributes.MOVEMENT_SPEED, (double)0.2F); + } +@@ -70,11 +122,12 @@ public class MagmaCube extends Slime { + } + + @Override +- protected void jumpFromGround() { ++ public void jumpFromGround() { // Purpur - protected -> public + Vec3 vec3 = this.getDeltaMovement(); + float f = (float)this.getSize() * 0.1F; + this.setDeltaMovement(vec3.x, (double)(this.getJumpPower() + f), 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 4a132c3eff6978e927bcd4df56b9ce0306af6d19..17638b9d3340c86528a8ae597712c7590b98dba6 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; ++ 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 ++ Vec3 crystalPosition; // Purpur + + public Phantom(EntityType type, Level world) { + super(type, world); +@@ -58,6 +60,92 @@ 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; ++ } ++ ++ @Override ++ protected void dropFromLootTable(DamageSource damageSource, boolean causedByPlayer) { ++ boolean dropped = false; ++ if (lastHurtByPlayer == null && damageSource.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) { ++ super.dropFromLootTable(damageSource, causedByPlayer); ++ } ++ } ++ ++ public boolean isCirclingCrystal() { ++ return crystalPosition != null; ++ } ++ // Purpur end ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.phantomTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.phantomAlwaysDropExp; + } + + @Override +@@ -72,9 +160,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 +186,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() { +@@ -120,6 +219,21 @@ public class Phantom extends FlyingMob implements Enemy { + 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 + public void tick() { + super.tick(); +@@ -140,14 +254,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 +271,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 +291,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 +308,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 +374,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 +391,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 +517,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 +575,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 +675,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 +826,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..06d52d8b61abc4dbbdc953bfed2e688be377b3cc 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 75d661c23cde1ba09ea0b673860c9659c32ef77e..dc55bf60f6e37971525214c1e872119e8fc92fe8 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(); ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 cdccbe68fb591181517893a5dd1e93489b578f4d..d1a80dfb51b86f92b37bbb4c235e38d2b8713541 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) { + Shulker entityshulker = (Shulker) EntityType.SHULKER.create(this.level()); ++ // Purpur end + + if (entityshulker != null) { + entityshulker.setVariant(this.getVariant()); +@@ -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 8b818a7cb835512c4bd2ea9641d4bfd904150332..f4a47ac2c86cc95178922cce7320ba1ef5121b57 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 343433158507451152e5b2fc5e5fd0b0e6b229db..467337542551dedc05d922bb3a37b811aeef4d7b 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 isSensitiveToWater() { ++ return this.level().purpurConfig.slimeTakeDamageFromWater; ++ } ++ ++ @Override ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.slimeAlwaysDropExp; ++ } ++ ++ @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; ++ } ++ // 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.onGround()) { +- 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 05432184077752b1d0cb764a5e39ed875748b2d6..b3ab0f8cdbd678970e39e89c2062772374466b67 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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..a95e983032b3d3d125a39a46700b7db9dc69f307 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + 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 ebf54d6e36fdee2833250816fae14195ac45eb67..cfa603c7ccaaf1940aa89fa7cd8fafba29529075 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + 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 65cb385ab294e362d666a6d03c4496cdc3b64890..5db70031c8c9ba901a360c758d51222efe911fcc 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 ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 2acc531bd9e948251cac77d979f973678f576394..f1e410b602bf0d5520f8e9340f7b8b9e25ca4d39 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 701f6bf1d558cf0ec4bc1abb9e1f66d96ea5a6c9..80e90202eae1e12d5261d0bb77ecfadcc9ac4599 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 9a81cb0c3a5ac40ff50dc7c81f6196a447cd03c6..2b3decb90aeb99bef42b36f3b8df06cb1172b413 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @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 d0cd8aa0515a314490e8d80f00e04115905e75aa..2745c5918181c9df15b7251b98f76c533293bf30 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @Override + protected Brain.Provider brainProvider() { + return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); +@@ -198,6 +230,7 @@ public class Zoglin extends Monster implements Enemy, HoglinBase { + + @Override + protected void customServerAiStep() { ++ if (getRider() == null || !this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level(), this); + 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 3f8c1d1d3c408fc4f15c4b5680bc22c86f104a9d..a0b3cc72fa857b3f4d3cbfa2e68226e8fb4d567f 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @Override + protected void registerGoals() { ++ this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + if (this.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 ( this.level().spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Spigot ++ // Purpur start ++ if ( this.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 + } + } + } +@@ -595,7 +634,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 087acc992a83b2a7e40a0e24f00e283cc139ce59..bf1967a933984db769513599ee7fdff7bc80bef1 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; ++ } ++ // Purpur end ++ ++ @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 isSensitiveToWater() { ++ return this.level().purpurConfig.zombieVillagerTakeDamageFromWater; ++ } ++ ++ @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 ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.zombieVillagerAlwaysDropExp; ++ } ++ + @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 aff140cfb2bbdce8b512080cf394c84c5c4ff807..7bb99d7fd8e05805e0cac7bec0b2771990057f58 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; ++ } ++ // Purpur end ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level().purpurConfig.zombifiedPiglinMaxHealth); ++ } ++ ++ @Override ++ public boolean isSensitiveToWater() { ++ return this.level().purpurConfig.zombifiedPiglinTakeDamageFromWater; ++ } ++ ++ @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 ++ protected boolean isAlwaysExperienceDropper() { ++ return this.level().purpurConfig.zombifiedPiglinAlwaysDropExp; ++ } ++ + @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 7387979e5b17994a48a86f37f81b170695b6ad8e..93c107f446bcccffb1bf92c4d8f4b8b1a8ef6d23 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @Override + public boolean canBeLeashed(Player player) { + return !this.isLeashed(); +@@ -129,7 +166,7 @@ public class Hoglin extends Animal implements Enemy, HoglinBase { + private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { +- 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); + HoglinAi.updateActivity(this); + if (this.isConverting()) { +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 f041a2b6b330692316e7c5385651d9370377d5e0..1bac25a9b4dd682d942ba5fce4cffb305a1d7532 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 +@@ -96,6 +96,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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @Override + public void addAdditionalSaveData(CompoundTag nbt) { + super.addAdditionalSaveData(nbt); +@@ -308,7 +340,7 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento + private int behaviorTick; // Pufferfish + @Override + protected void customServerAiStep() { +- 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); + PiglinAi.updateActivity(this); + super.customServerAiStep(); +@@ -405,7 +437,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 5aab051998b67b7ba95cbf568de60e325b905eab..1805d4e15328fa99595641ea64fc2e3e659c555e 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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + 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); + } +@@ -85,6 +117,7 @@ public class PiglinBrute extends AbstractPiglin { + + @Override + protected void customServerAiStep() { ++ if (getRider() == null || this.isControllable()) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel)this.level(), this); + PiglinBruteAi.updateActivity(this); + PiglinBruteAi.maybePlayActivitySound(this); +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 55d5aad6ee98bc61dac415b106d0b6d1048dae7e..56bbee840ad585148943e52cf2ddc98baba93fc5 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 +@@ -121,8 +121,32 @@ public class Warden extends Monster implements VibrationSystem { + 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); +@@ -394,19 +418,16 @@ public class Warden extends Monster implements VibrationSystem { + + @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 564908ce0a560c2190fb624e77d227d3b7031024..f2a4e214744227f1df32e3782e71f8a9d5cea261 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 ffe93e11c1ab6986ea73f486fbc475aca51eca4a..d7674e9e14000a1351e43d75df4ccaab0745fce2 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -142,6 +142,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 + +@@ -156,6 +158,90 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + this.getNavigation().setCanFloat(true); + this.setCanPickUpLoot(true); + this.setVillagerData(this.getVillagerData().setType(type).setProfession(VillagerProfession.NONE)); ++ if (level().purpurConfig.villagerFollowEmeraldBlock) this.goalSelector.addGoal(3, new net.minecraft.world.entity.ai.goal.TemptGoal(this, 1.0D, TEMPT_ITEMS, false)); ++ } ++ ++ // 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)); ++ } ++ // Purpur end ++ ++ @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; + } + + @Override +@@ -192,7 +278,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)); +@@ -255,15 +341,23 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + // Paper start + this.customServerAiStep(false); + } +- protected void customServerAiStep(final boolean inactive) { ++ protected void customServerAiStep(boolean inactive) { // Purpur - not final + // Paper end + if (!inactive) this.getBrain().tick((ServerLevel) this.level(), this); // Paper ++ // Purpur start + // Pufferfish start +- if (!inactive) { +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ if (this.level().purpurConfig.villagerLobotomizeEnabled) { ++ // treat as inactive if lobotomized ++ inactive = inactive || checkLobotomized(); ++ } else { ++ // clean up state for API ++ this.isLobotomized = false; ++ if (!inactive && (getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Purpur - only use brain if no rider + this.getBrain().tick((ServerLevel) this.level(), this); // Paper ++ else if (this.isLobotomized && shouldRestock()) restock(); ++ // Pufferfish end ++ // Purpur end + } +- // Pufferfish end + if (this.assignProfessionWhenSpawned) { + this.assignProfessionWhenSpawned = false; + } +@@ -319,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(); + +@@ -332,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()) { + this.startTrading(player); + } + +@@ -503,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 + } + + } +@@ -753,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() { +@@ -967,6 +1062,11 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + + public boolean hasFarmSeeds() { + return this.getInventory().hasAnyMatching((itemstack) -> { ++ // Purpur start ++ if (this.level().purpurConfig.villagerClericsFarmWarts && this.getVillagerData().getProfession() == VillagerProfession.CLERIC) { ++ return itemstack.is(Items.NETHER_WART); ++ } ++ // Purpur end + return itemstack.is(ItemTags.VILLAGER_PLANTABLE_SEEDS); + }); + } +@@ -1016,6 +1116,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); +@@ -1089,6 +1190,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 95197b601d93c30a7645d67c89c7608fc00a8de6..3ccfac9e8ae958f3f94fe774120716e5b6293ce7 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java ++++ b/src/main/java/net/minecraft/world/entity/npc/VillagerProfession.java +@@ -1,6 +1,8 @@ + package net.minecraft.world.entity.npc; + + import java.util.function.Predicate; ++ ++import com.google.common.collect.ImmutableSet; + import net.minecraft.core.Holder; + import net.minecraft.core.Registry; + import net.minecraft.core.registries.BuiltInRegistries; +@@ -26,7 +28,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).toArray(new Item[0]), Blocks.SOUL_SAND, SoundEvents.VILLAGER_WORK_CLERIC); // Purpur + public static final VillagerProfession FARMER = register("farmer", PoiTypes.FARMER, new Item[] {Items.WHEAT, Items.WHEAT_SEEDS, Items.BEETROOT_SEEDS, Items.BONE_MEAL}, Blocks.FARMLAND, SoundEvents.VILLAGER_WORK_FARMER); // Gale - optimize villager data storage + 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 2e7de2378e01aed514e237029d6d64e78871c9b4..ecfb5d8b47c461ae661149f1c7b9bacee226c311 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java +@@ -66,6 +66,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; ++ } ++ // Purpur end ++ ++ @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; ++ } ++ + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); +@@ -73,7 +110,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 this.canDrinkMilk && this.level().isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API ++ return level().purpurConfig.milkClearsBeneficialEffects && this.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)); +@@ -86,6 +123,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)); +@@ -113,9 +151,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) { + 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 f070bd0eca4a55445f436c9520a89aabb5f55d12..c02684fcb22c679eb902e2cf3714345ababd5805 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -189,17 +189,42 @@ 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 int burpDelay = 0; // Purpur ++ public boolean canPortalInstant = false; // Purpur + + // CraftBukkit start + public boolean fauxSleeping; + public int oldLevel = -1; + ++ // Purpur start ++ public void setAfk(boolean afk) { ++ } ++ ++ public boolean isAfk() { ++ return false; ++ } ++ // Purpur end ++ + @Override + public CraftHumanEntity getBukkitEntity() { + return (CraftHumanEntity) super.getBukkitEntity(); + } + // CraftBukkit end + ++ // Purpur start ++ public abstract void resetLastActionTime(); ++ ++ @Override ++ public boolean processClick(InteractionHand hand) { ++ Entity vehicle = getRootVehicle(); ++ if (vehicle != null && vehicle.getRider() == this) { ++ return vehicle.onClick(hand); ++ } ++ return false; ++ } ++ // Purpur end ++ + public final int sendAllPlayerInfoBucketIndex; // Gale - Purpur - spread out sending all player info + + public Player(Level world, BlockPos pos, float yaw, GameProfile gameProfile) { +@@ -247,6 +272,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.setOnGround(false); +@@ -355,6 +386,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() { +@@ -441,7 +482,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 +@@ -601,7 +642,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); +@@ -1292,7 +1333,7 @@ public abstract class Player extends LivingEntity { + flag2 = flag2 && !this.level().paperConfig().entities.behavior.disablePlayerCrits; // Paper + flag2 = flag2 && !this.isSprinting(); + if (flag2) { +- f *= 1.5F; ++ f *= this.level().purpurConfig.playerCriticalDamageMultiplier; // Purpur + } + + f += f1; +@@ -1992,9 +2033,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; + } +@@ -2070,6 +2121,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()) { +@@ -2354,7 +2410,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..862972afa333422592a25b854cec191e02c10734 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.matches(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 cff4d09af11d1741bf3301b457555d71e77e801c..2bca617a1f0198c5ddba1be09cc498b3364629b6 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 +@@ -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 251e6bc87eb02ec4372062ef22b21249ac5164ff..6b60597a0e837054b0d1891c1e6e5e7cd3af9539 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 0bbe853f7df93f9dcd2b21d762939f8b6be069aa..7db9844083703944f59e112c6dc5e1a5e062d31c 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 d598a99072bd8eb4a4ed151b9202237dd0c2af8c..5a40922128fc7722afd9e902c1502a038cb57a25 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -328,6 +328,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/ProjectileUtil.java b/src/main/java/net/minecraft/world/entity/projectile/ProjectileUtil.java +index cc0a3d9794d05b6bc6ab05f4f2ab8d83134b181d..e1f918d0bd2a70db1aba8bda8717149f58766825 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ProjectileUtil.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ProjectileUtil.java +@@ -33,7 +33,7 @@ public final class ProjectileUtil { + return getHitResult(vec32, entity, predicate, vec3, level); + } + +- private static HitResult getHitResult(Vec3 pos, Entity entity, Predicate predicate, Vec3 velocity, Level world) { ++ public static HitResult getHitResult(Vec3 pos, Entity entity, Predicate predicate, Vec3 velocity, Level world) { // Purpur - private -> public + Vec3 vec3 = pos.add(velocity); + HitResult hitResult = world.clip(new ClipContext(pos, vec3, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, entity)); + if (hitResult.getType() != HitResult.Type.MISS) { +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 04c2ea1ff44af72ae48e2d6b7b912b1c14285038..7839d856b3f924f0c87b298d9a1e3b15ab0693d6 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 718e120c9768cf716b32d3d652f53f1dda925168..9a8a1e773f247f4c4af4e9789deb70cd5b7889c5 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 e8114d89a3129e56c0329410a49ded63cc77cb4c..d7359c675707eade00f9b737fd67ef8d066e813f 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 454dd67920826b8b62c2654abfd43fc08c2648e4..0ea182962d6647629fc98c9e7406f7b7ce012c5e 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 80b86d5dd68c3d288a1a61ea8aa1cba9d899aa1c..7847915fb90e45e44ba514957193fa7b747e0c76 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 e9e984959e08c139c8f2b4b9611bdba345ca9a13..16d7fa4d3a7c507d4933444fc0af094d82c3bbca 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.LEADER_BANNER)) { // Gale - Lithium - cache ominous banner item +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 41457c9f27b18fa2734a6cca297ec5186470e82f..94356e0541f8f4da68211fa533347cc97d4f3518 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 9948a28dae4edba877c13ef0156be5ff58df3fa2..b99b94c6ec4767aba16d82eaca8b2761d779b226 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecart.java +@@ -108,11 +108,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) { +@@ -335,6 +337,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(); +@@ -499,16 +507,62 @@ 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() { ++ BlockState blockState = level().getBlockState(this.blockPosition()); ++ if (!blockState.isSolid()) { ++ blockState = level().getBlockState(this.blockPosition().relative(Direction.DOWN)); ++ } ++ Double speed = level().purpurConfig.minecartControllableBlockSpeeds.get(blockState.getBlock()); ++ if (!blockState.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()) { +@@ -670,7 +724,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 35aeba4e8430e6419caa9db4a0b931a994228618..a941b00f0e4b667925ca68cc706245b05478ad77 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 2038df72f8d7d33d4105de8129628daf21de6f0f..e54af9ff2a786e919b8261aa27509be942e70261 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 706b354ac9a1a6a4a1e61b2a109180d1dd22bbbd..9ca261c9f21279558961649cb4849ac379d67573 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 35b41bceeba72c3896c91c2605bac3b0bf9c54e9..5c028f957661089ff502109c996692856b12ee27 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 e0c3a4ba27e21c3692e601acd0af60873bcbb84c..cb4a1cd39f12d90d30dd95efa2de58f57d9c6ee0 100644 +--- a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java +@@ -23,6 +23,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; +@@ -51,6 +58,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); +@@ -78,12 +87,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()); + } + +@@ -134,6 +146,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); +@@ -210,7 +228,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; +@@ -222,16 +241,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(); + } + +@@ -276,6 +295,54 @@ public class AnvilMenu extends ItemCombinerMenu { + 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)); + } + } else if (itemstack.hasCustomHoverName()) { +@@ -293,6 +360,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; + } +@@ -315,11 +389,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 boolean 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 93e8316f9d64625dcc4df0644a2187bcc884ef65..1711406996ec4a9cb7b1838bd2446e5cf802e043 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().level().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) { +@@ -340,6 +373,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 b56766ff0e61691294b40ea8c2370940c0e8b640..23a8522b80475ad29ffb4afd2f4836acda2538e3 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()); + } + } +@@ -250,7 +252,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + } + + 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); +@@ -266,6 +268,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; + } + +@@ -327,7 +343,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 c549618421c5d077c3d977d8d2064eca2acc438a..5972fa434847d24fa98b7895fd8386d20a636885 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..9ecc80c7d1fa1bccf0c44c812274c8f6292cebc7 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 578c3db52dda4c169b5ea615a4ce4a79f15a4cad..0bd98b802f246a3f6061f716d470a4797b28d59d 100644 +--- a/src/main/java/net/minecraft/world/item/BucketItem.java ++++ b/src/main/java/net/minecraft/world/item/BucketItem.java +@@ -164,7 +164,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(); +@@ -172,7 +172,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 a433910fb4d0bd8d7b6b0d66c8fc88d62a0e4879..73275b38c08a5ab85538625510fd8aac59f9a092 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()) { +@@ -114,7 +114,7 @@ public class CrossbowItem extends ProjectileWeaponItem implements Vanishable { + // Paper end + int i = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.MULTISHOT, crossbow); + 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, crossbow) > 0); // Paper - add consume // Purpur + ItemStack itemstack1 = shooter.getProjectile(crossbow); + ItemStack itemstack2 = itemstack1.copy(); + +@@ -295,6 +295,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; + } + +@@ -304,7 +312,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 88e1c2431d51d8cdc3d555b711e506648225d289..ac8735cc9d127fc1f867b40d4000c033ef73bb83 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 a4b51ca1a778efa3f4208d18b2c93a6e733ed987..b590ce7a7a2eaf7d50ca83673bb85186123cba97 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -113,6 +113,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::getItem), Codec.INT.fieldOf("Count").forGetter(ItemStack::getCount), CompoundTag.CODEC.optionalFieldOf("tag").forGetter((itemstack) -> { + return Optional.ofNullable(itemstack.getTag()); +@@ -421,6 +422,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; + +@@ -450,6 +452,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 + } +@@ -578,6 +581,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"); + } +@@ -597,7 +610,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) { +@@ -652,6 +665,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); +@@ -1178,7 +1197,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 + } + +@@ -1186,6 +1205,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 5f20e075c532f0f1d413242949d1738c0c152bf7..5fbb13ebef0ca66419f3e5006d19e4a5918a038a 100644 +--- a/src/main/java/net/minecraft/world/item/Items.java ++++ b/src/main/java/net/minecraft/world/item/Items.java +@@ -294,7 +294,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); +@@ -1184,7 +1184,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); +@@ -1309,6 +1309,13 @@ public class Items { + ((BlockItem)item).registerBlocks(Item.BY_BLOCK, item); + } + ++ // Purpur start ++ if (item.getFoodProperties() != null) { ++ Foods.ALL_PROPERTIES.put(key.location().getPath(), item.getFoodProperties()); ++ Foods.DEFAULT_PROPERTIES.put(key.location().getPath(), item.getFoodProperties().copy()); ++ } ++ // Purpur end ++ + return Registry.register(BuiltInRegistries.ITEM, key, 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..cef98413d25dcc2def82775bbae71f92b096d905 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.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 2941c16ef486345b57ab2dfcd26f0272285d3b5a..7cc6812bf6f2ba015f65fd1fc1eaac02dd0f53e2 100644 +--- a/src/main/java/net/minecraft/world/item/NameTagItem.java ++++ b/src/main/java/net/minecraft/world/item/NameTagItem.java +@@ -20,6 +20,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 ? io.papermc.paper.adventure.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 741719301e6fc91a598e74342810c4185e6fde26..6fbff9c02fbabf03c9c649a9ea6128021081f9cd 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 8078f127ff4b6e0aafb5804b9c02e237f79445b5..c32cbe6065ecb6810f352b8a3598c21e42e60e1d 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.onGround()) { +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 cd7ea0c16f9ddcb84b5d7e8a2533e6e84f3879c7..f47eab4c31925f51de4a6bc8be730281cb3388fc 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 28bdcb14cb5b458d3c990fcf343ef97f08e4f3c6..48167334162443365bb8a6d082a51b2c626ab3d8 100644 +--- a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java ++++ b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java +@@ -134,7 +134,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 633500aefd515df5dadda3802b94079f75a03fa0..64d911bee1607880514061c75116d8672df8bb8f 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 2a7813209d664fd8c123488ce9c530b4440be163..a3eaeede3ee07a1e7e9a3493c5ae32553962b8f5 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; +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 4b8736f19c0b968fac27037d2d0eb3f6d3a2fe94..3d10416db217d775adbf6a2f099095f0d43c02ee 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -174,6 +174,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Gale end - Gale configuration + + 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; +@@ -182,7 +183,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions + public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Move from Map in BlockRedstoneTorch to here + +- public final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); // Gale - Pufferfish - move random tick random ++ public final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); // Gale - Pufferfish - move random tick random // Purpur - dont break ABI // Leaf - idk why? + + // Paper start - fix and optimise world upgrading + // copied from below +@@ -193,6 +194,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; + } +@@ -286,6 +330,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper + this.galeConfig = galeWorldConfigCreator.apply(this.spigotConfig); // Gale - Gale configuration ++ 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); + +@@ -1556,4 +1602,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 135c62a90b1cf924812fa4c8c224057eed67a109..38a2456da7c5fa4a6df0884866b3a44936ce0102 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -258,7 +258,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 d40500f9a807cab0b2fb6fa9032f33f4fb74c895..2b66ddafaaca17f64d1e7502dfa4d7576e3e032f 100644 +--- a/src/main/java/net/minecraft/world/level/block/BedBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BedBlock.java +@@ -96,7 +96,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 +@@ -149,7 +149,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; + } + } +@@ -173,7 +173,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 7455847cf8a05ed237d2df84ae2d9b1d2f2d7176..2960f5e35012665939c2d670d212aa4d6cb228db 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 e9d37ab8e2b072454c46d4520491beac5873a7ec..43c15ce83854904e3b998f61dd31cd257a5bb6bf 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -64,6 +64,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(); +@@ -90,6 +97,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 || +@@ -316,7 +327,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); + } +@@ -332,7 +343,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); + } +@@ -343,13 +354,53 @@ public class Block extends BlockBehaviour implements ItemLike { + public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable 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); +@@ -425,7 +476,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(BlockState state) { + return !state.isSolid() && !state.liquid(); +@@ -444,7 +505,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 877035b6c6593a28f475b9c5bcd7727e3fcdb802..ed35878fdb9dffcd46c27d26ee8379401207cef5 100644 +--- a/src/main/java/net/minecraft/world/level/block/Blocks.java ++++ b/src/main/java/net/minecraft/world/level/block/Blocks.java +@@ -1093,8 +1093,8 @@ public class Blocks { + public static final Block CAVE_VINES = register("cave_vines", new CaveVinesBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).randomTicks().noCollission().lightLevel(CaveVines.emission(14)).instabreak().sound(SoundType.CAVE_VINES).pushReaction(PushReaction.DESTROY))); + public static final Block CAVE_VINES_PLANT = register("cave_vines_plant", new CaveVinesPlantBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().lightLevel(CaveVines.emission(14)).instabreak().sound(SoundType.CAVE_VINES).pushReaction(PushReaction.DESTROY))); + public static final Block SPORE_BLOSSOM = register("spore_blossom", new SporeBlossomBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).instabreak().noCollission().sound(SoundType.SPORE_BLOSSOM).pushReaction(PushReaction.DESTROY))); +- public static final Block AZALEA = register("azalea", new AzaleaBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).forceSolidOff().instabreak().sound(SoundType.AZALEA).noOcclusion().pushReaction(PushReaction.DESTROY))); +- public static final Block FLOWERING_AZALEA = register("flowering_azalea", new AzaleaBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).forceSolidOff().instabreak().sound(SoundType.FLOWERING_AZALEA).noOcclusion().pushReaction(PushReaction.DESTROY))); ++ public static final Block AZALEA = register("azalea", new AzaleaBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).forceSolidOff().randomTicks().instabreak().sound(SoundType.AZALEA).noOcclusion().pushReaction(PushReaction.DESTROY))); // Purpur ++ public static final Block FLOWERING_AZALEA = register("flowering_azalea", new AzaleaBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).forceSolidOff().randomTicks().instabreak().sound(SoundType.FLOWERING_AZALEA).noOcclusion().pushReaction(PushReaction.DESTROY))); // Purpur + public static final Block MOSS_CARPET = register("moss_carpet", new CarpetBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GREEN).strength(0.1F).sound(SoundType.MOSS_CARPET).pushReaction(PushReaction.DESTROY))); + public static final Block PINK_PETALS = register("pink_petals", new PinkPetalsBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.PINK_PETALS).pushReaction(PushReaction.DESTROY))); + public static final Block MOSS_BLOCK = register("moss_block", new MossBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GREEN).strength(0.1F).sound(SoundType.MOSS).pushReaction(PushReaction.DESTROY))); +@@ -1159,7 +1159,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/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 0003fb51ae3a6575575e10b4c86719f3061e2577..0d5f87d24231f6d2b8639825bcd62dd2f8791c8e 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; +@@ -107,7 +107,7 @@ public class CactusBlock extends Block { + + enumdirection = (Direction) iterator.next(); + iblockdata1 = world.getBlockState(pos.relative(enumdirection)); +- } while (!iblockdata1.isSolid() && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); ++ } while ((!world.getWorldBorder().world.purpurConfig.cactusBreaksFromSolidNeighbors || !iblockdata1.isSolid()) && !world.getFluidState(pos.relative(enumdirection)).is(FluidTags.LAVA)); // Purpur + + return false; + } +@@ -129,4 +129,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 23c487e295b3b736d8800f0c884324c9b18a5373..ebeb7caf7fd4f45714bab0856a48b847a544cce7 100644 +--- a/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CarvedPumpkinBlock.java +@@ -64,7 +64,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + 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); +@@ -74,7 +74,7 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + + 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 + } + } + } +@@ -82,6 +82,16 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + } + + 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 8d741018c7dc9a2ac1b666025a4e041010a745d4..1a3d1f66a65c9627d2bd29476bbac788fb391eae 100644 +--- a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java +@@ -232,20 +232,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) { +- BlockState iblockdata1 = ComposterBlock.addItem(player, state, world, pos, itemstack); +- // Paper start - handle cancelled events +- if (iblockdata1 == null) { +- return InteractionResult.PASS; +- } +- // Paper end ++ // Purpur start ++ BlockState newState = process(i, state, world, itemstack, pos, player); ++ if (newState == null) { ++ return InteractionResult.PASS; ++ } + +- world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0); +- player.awardStat(Stats.ITEM_USED.get(itemstack.getItem())); +- if (!player.getAbilities().instabuild) { +- itemstack.shrink(1); +- } ++ 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) { +@@ -256,6 +264,26 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + ++ private static BlockState process(int level, BlockState state, Level world, ItemStack itemstack, BlockPos pos, Player player) { ++ if (level < 7 && !world.isClientSide) { ++ BlockState iblockdata1 = ComposterBlock.addItem(player, state, world, pos, itemstack); ++ // Paper start - handle cancelled events ++ if (iblockdata1 == null) { ++ return iblockdata1; ++ } ++ // 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 iblockdata1; ++ } ++ 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 c463adf131c4ca6e38f18d4efd94f4629bcfafe9..f1016932cba6905c9cd474daeae648b2dcc1f32d 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 c028a7158e41a0754abb8e24dcd647633fbf3fe8..cd65d32f4af016d4937e598c71386a3072f4c490 100644 +--- a/src/main/java/net/minecraft/world/level/block/DoorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DoorBlock.java +@@ -167,6 +167,7 @@ public class DoorBlock extends Block { + public InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!this.type.canOpenByHand()) { + 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); +@@ -270,4 +271,18 @@ public class DoorBlock extends Block { + flag = false; + return flag; + } ++ ++ // 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 839b7bc9392906dca384003468746963631fe095..286f34eef22a85be3fe9747dc3c3f9a7d51f437c 100644 +--- a/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EnchantmentTableBlock.java +@@ -29,6 +29,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); +@@ -121,4 +123,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 552d8c8f3f56bfccd25d11488ed7ec1644a92f47..2b1bd583d9c7f049bfb798aa38ef021a881267e9 100644 +--- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +@@ -101,7 +101,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) { +@@ -115,6 +115,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; + } +@@ -162,7 +178,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 5fbdc96f29e29dfc092b9e84a988032db0fa36ab..1352168db8f3062943cc05396cbd9b535508c461 100644 +--- a/src/main/java/net/minecraft/world/level/block/IceBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/IceBlock.java +@@ -33,7 +33,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; + } +@@ -61,7 +61,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, IceBlock.meltsInto()); +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 bbabe4ad8afcc3a2069f6e9d4a9adcb643266894..55419bd653f7f5391fa13cd15a0b00fbff5e9c39 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 + } + +@@ -133,7 +133,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)); + } + +@@ -142,7 +142,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 1b766045687e4dcded5cbcc50b746c55b9a34e22..be365914856593bb3c4e1945cc990786072f2953 100644 +--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +@@ -22,7 +22,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 910864cfeac085648e6c671b0f9480417324d36e..244857755f269eedd57d1c8d4657b4b4b75de2e5 100644 +--- a/src/main/java/net/minecraft/world/level/block/NoteBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NoteBlock.java +@@ -58,11 +58,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 = direction.getAxis() == Direction.Axis.Y; + + return flag ? this.setInstrument(world, pos, state) : super.updateShape(state, direction, neighborState, world, pos, neighborPos); +@@ -78,13 +80,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)).worksAboveNoteBlock() || world.getBlockState(pos.above()).isAir()) { ++ if (world.purpurConfig.noteBlockIgnoreAbove || ((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).worksAboveNoteBlock() || 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 e978132e51cde52f7ff1ba31ad521fc2cb4f0dce..60a95c42a333d68d9fd14539e0051c0851d96e3b 100644 +--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +@@ -186,7 +186,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); + +@@ -195,13 +195,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 02d01eabb9606ae8c3b76ad9fa4bb9a525e247b1..ce51fec4a874f9466f9966684c535315dbf40b9e 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 b51155ad12515b2d0dd0f202580b9f455c114d9a..dd6c82a418ee299d7a5614cb0260949c198b4149 100644 +--- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java +@@ -137,7 +137,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/SlabBlock.java b/src/main/java/net/minecraft/world/level/block/SlabBlock.java +index 18b603d646081926343dea108b55d641df1c2c34..370772b1297b78bcc7419684015830a87c4d9a17 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 4bce895268542531598a01a1bccd8ac1ed703b7d..5964949a6048194476e6dd501503caf0a206715a 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpongeBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpongeBlock.java +@@ -48,7 +48,7 @@ public class SpongeBlock extends Block { + + private boolean removeWaterBreadthFirstSearch(Level world, BlockPos pos) { + BlockStateListPopulator blockList = new BlockStateListPopulator(world); // CraftBukkit - Use BlockStateListPopulator +- BlockPos.breadthFirstTraversal(pos, 6, 65, (blockposition1, consumer) -> { ++ BlockPos.breadthFirstTraversal(pos, world.purpurConfig.spongeAbsorptionRadius, 65, (blockposition1, consumer) -> { // Purpur + Direction[] aenumdirection = SpongeBlock.ALL_DIRECTIONS; + int i = aenumdirection.length; + +@@ -116,7 +116,7 @@ public class SpongeBlock extends Block { + BlockState iblockdata = world.getBlockState(blockposition1); + FluidState fluid = world.getFluidState(blockposition1); + +- 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, blockposition1, iblockdata).isEmpty()) { + // NOP + } else if (iblockdata.getBlock() instanceof LiquidBlock) { +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 c3f500580d257e1397f2eb7c47b063a6fe6bb405..0d5c6bdfd4aeda472804b493315bf21ac3067e9d 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 1aa0e921890d600c9274deb923da04e72b12bcc6..44bd7bee2665a05878fd2df935a700f02cd13a75 100644 +--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -69,6 +69,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 381818dddc00be81d81456161fe6c6a29603987c..81f684c1afb7d693d8546e4827944c8da2a8c3f5 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 +@@ -46,6 +46,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; +@@ -209,6 +210,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(); +@@ -326,6 +343,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(); + +@@ -411,6 +443,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 59246e24558569f7f50b4d4d508616798091c888..49a2308832b24dc1eb839af786dbec9f046bdab6 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 370a25d2deb54f10a35ee24d9e7e92fbfde60edf..3431f1a00ae2918b91a6b7a449e613e6e12ff6d4 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; +@@ -73,10 +75,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(); +@@ -186,10 +205,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(); + } + +@@ -263,4 +296,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 ba6f0ba00cd2635bcd19889dca0349b363b079fe..86fc8079faa4b110387e98c100d423485aae0ea0 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,11 +203,18 @@ public class SignBlockEntity extends BlockEntity implements CommandSource { // C + FilteredText filteredtext = (FilteredText) list.get(i); + Style chatmodifier = signtext.getMessage(i, entityhuman.isTextFilteringEnabled()).getStyle(); + +- if (entityhuman.isTextFilteringEnabled()) { +- signtext = signtext.setMessage(i, Component.literal(net.minecraft.SharedConstants.filterText(filteredtext.filteredOrEmpty())).setStyle(chatmodifier)); // Paper - filter sign text to chat only ++ // Purpur start TODO: This is probably wrong but we'll see what happens ++ org.bukkit.entity.Player player = (org.bukkit.craftbukkit.entity.CraftPlayer) entityhuman.getBukkitEntity(); ++ String line = net.minecraft.SharedConstants.filterText(entityhuman.isTextFilteringEnabled() ? filteredtext.filteredOrEmpty() : filteredtext.raw()); ++ if (level.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"); ++ signtext = signtext.setMessage(i, io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(line)).copy().setStyle(chatmodifier)); + } else { +- signtext = signtext.setMessage(i, Component.literal(net.minecraft.SharedConstants.filterText(filteredtext.raw())).setStyle(chatmodifier), Component.literal(net.minecraft.SharedConstants.filterText(filteredtext.filteredOrEmpty())).setStyle(chatmodifier)); // Paper - filter sign text to chat only ++ signtext = signtext.setMessage(i, io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.Component.text(line)).copy().setStyle(chatmodifier)); + } ++ // Purpur end + } + + // CraftBukkit start +@@ -345,6 +352,23 @@ public class SignBlockEntity extends BlockEntity implements CommandSource { // C + return ClientboundBlockEntityDataPacket.create(this); + } + ++ // Purpur start ++ public ClientboundBlockEntityDataPacket getTranslatedUpdatePacket(boolean filtered, boolean front) { ++ final CompoundTag nbt = new CompoundTag(); ++ this.saveAdditional(nbt); ++ final Component[] lines = front ? frontText.getMessages(filtered) : backText.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 cc17cb985856fd8d1083b5b1f57f82c651d28443..33c82b44f184b3797e1c290dbbff5bf360ed30ae 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 + if (entity.level().galeConfig().gameplayMechanics.fixes.checkCanChangeDimensionsBeforeUseEndGateway && world.galeConfig().gameplayMechanics.fixes.checkCanChangeDimensionsBeforeUseEndGateway && !entity.canChangeDimensions()) return; // Gale - Purpur - end gateway should check if entity can use portal + ServerLevel worldserver = (ServerLevel) world; + +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 de4c1e4701236e7d5ec77339c51ad6a9d8288bb6..942ce713afe27ec75d849877a88721ef6334fafa 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 +@@ -81,7 +81,7 @@ 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}; + 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; +@@ -89,7 +89,7 @@ public abstract class BlockBehaviour implements FeatureElement { + protected final float jumpFactor; + protected final boolean dynamicShape; + protected final FeatureFlagSet requiredFeatures; +- protected final BlockBehaviour.Properties properties; ++ public final BlockBehaviour.Properties properties; // Purpur - protected -> public + @Nullable + protected ResourceLocation drops; + +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 901938201c1abee1c88e217d2c1ba1a2d147420e..6530e172527ad4c42a65f03ce8d4847facf43b38 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -123,7 +123,7 @@ public class LevelChunk extends ChunkAccess { + this.postLoad = entityLoader; + this.blockTicks = blockTickScheduler; + this.fluidTicks = fluidTickScheduler; +- this.lightningTick = this.level.randomTickRandom.nextInt(100000) << 1; // Gale - Airplane - optimize random calls in chunk ticking - initialize lightning tick ++ this.lightningTick = java.util.concurrent.ThreadLocalRandom.current().nextInt(100000) << 1; // Gale - Airplane - optimize random calls in chunk ticking - initialize lightning tick // Purpur - any random will do + } + + // CraftBukkit start +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 11f6a1854c08cbf8ee5c99522c3cd2ae5322de6d..21a8f38d9f033bc443de19ba47a16478e677929c 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -49,7 +49,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; +@@ -61,10 +61,10 @@ public class PhantomSpawner implements CustomSpawner { + if (!entityplayer.isSpectator() && (!world.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !entityplayer.isCreative())) { // Paper + BlockPos blockposition = entityplayer.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 = entityplayer.getStats(); + int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE); + boolean flag2 = true; +@@ -84,7 +84,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 974cd5ff0836d89127be190b1335d945df1cb73c..05334f99e6ef1bb9449e352b79b21c0d2fded2d7 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -227,7 +227,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(); + +@@ -325,6 +325,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 72f8b72c6436ca3b8eaeb39c7d3efe2c1462ae1d..f3d4a4196847e26934b6d2ed592f0ddb0e53182b 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/WalkNodeEvaluator.java b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +index 3583fcf5284bc5883308876dbd9886664b391e28..ba57accc272958da4714896baeadb52c99383561 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +@@ -241,7 +241,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; +@@ -465,7 +465,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; + } + +@@ -498,7 +498,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/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 bca065a167a1bf0581559f2e578c112fdf570c8a..5fabbef99fe1c38a4b52461d9c107c51ecb9b6e5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -327,6 +327,20 @@ public final class CraftServer implements Server { + this.dataPackManager = new CraftDataPackManager(this.getServer().getPackRepository()); + + 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(); +@@ -984,6 +998,7 @@ public final class CraftServer implements Server { + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot + this.console.paperConfigurations.reloadConfigs(this.console); + this.console.galeConfigurations.reloadConfigs(this.console); // Gale - Gale configuration ++ 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)) +@@ -999,6 +1014,7 @@ public final class CraftServer implements Server { + } + } + world.spigotConfig.init(); // Spigot ++ world.purpurConfig.init(); // Purpur + } + + Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper +@@ -1014,6 +1030,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"); + +@@ -1454,6 +1471,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) { + Preconditions.checkArgument(result != null, "ItemStack cannot be null"); +@@ -2739,6 +2805,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() +@@ -2831,6 +2898,18 @@ public final class CraftServer implements Server { + } + // Gale end - Gale configuration - API + ++ // 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(); +@@ -3062,4 +3141,15 @@ public final class CraftServer implements Server { + } + // Gale end - YAPFA - last tick time - API + ++ // 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 1a481fb4f4228f0fea8a7dc6132248c98b727c90..1fc38937f514de993439487bb5ec492b49a2ad0b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2290,6 +2290,48 @@ public class CraftWorld extends CraftRegionAccessor implements World { + return (this.getHandle().getDragonFight() == null) ? null : new CraftDragonBattle(this.getHandle().getDragonFight()); + } + ++ // 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 66932eefca3b7b7ec0103e19a38b010ae1e2845a..f49f1eef51e14e9bf94686bb795be4f264a50944 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -194,6 +194,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() +@@ -305,7 +319,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 733158b6f2c2bd03fbe798562ff7bc33280548dc..37739d0a58fa60f79462c577531d3e4b5e47b2ba 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -209,6 +209,21 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + this.entity = entity; + } + ++ @Override ++ public boolean isImmuneToFire() { ++ return getHandle().fireImmune(); ++ } ++ ++ @Override ++ public void setImmuneToFire(Boolean fireImmune) { ++ getHandle().immuneToFire = fireImmune; ++ } ++ ++ @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 +601,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; + } + +@@ -1435,4 +1454,27 @@ 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(); ++ } ++ // 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 0f3e696582cd288357598490e8af0131ab30bdfc..355797357f1c1cfceb8dbc016ef1b675c9a0759a 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 7880631fe8a4b06f29ef69ab850129737a99521b..4f6a1bf6edf6b0a280cb1647c9c9f3bbe5c54054 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +@@ -440,7 +440,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 + +@@ -452,7 +452,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; + } + +@@ -473,7 +473,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 +@@ -485,7 +485,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; + } +@@ -1065,4 +1065,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 03bb444705916ffe0b9eb4b7496524dc3459ebe0..a1bd3231001a57dd247d7e3fbb5b7e0bc590e58d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -498,10 +498,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())); +@@ -1322,6 +1327,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; + } + +@@ -2366,6 +2375,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) { + Preconditions.checkArgument(value <= 1f && value >= -1f, "Speed value (%s) need to be between -1f and 1f", value); + } +@@ -3138,4 +3169,90 @@ 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) { ++ if (this.getHandle().connection == null) return; ++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket(getEntityId(), io.papermc.paper.adventure.PaperAdventure.asVanilla(message))); ++ } ++ // 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 f21c715ea109164efa755cddb35ef656a9859759..0c9154d1d3c1534a25c08c75fab102b68958fab9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +@@ -222,4 +222,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 1477c2c04d8f5c5639ce94808fe2a7029cedaeb2..e333c591582ac196f2cea1b69d39117092e80a3a 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 32fe1e498ee988566213bfcf56bf98c92a3c9871..5374640d92cdf80eb1fdfc21c9076a988222c6d8 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; + } + +@@ -1177,6 +1188,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 3075ba5f6d66316f27e618d8b279252e9520b9cb..299a099ead00069cc275e1dc9a21742abf222d85 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +@@ -181,8 +181,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 df254c42b73cdb56f71781473cbf9d0f28dcfb08..483cc1ff49b2a4fc62173e756e1cbe4789f3cd60 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) { +- Preconditions.checkArgument(items.length <= this.getSize(), "Invalid inventory size (%s); expected %s or less", items.length, this.getSize()); ++ // Preconditions.checkArgument(items.length <= this.getSize(), "Invalid inventory size (%s); expected %s or less", items.length, this.getSize()); // Purpur + + for (int i = 0; i < this.getSize(); i++) { + if (i >= items.length) { +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 d94ab2607d1ab657a6b37924ce5ebcbbc3984011..f594596029513426f974ba820ed4702703b667a4 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.CompoundTag; + import net.minecraft.nbt.ListTag; + 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 +@@ -91,7 +93,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 + } + } + } +@@ -138,6 +146,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); + } + } +@@ -199,7 +212,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 13d25d118eb4d3ef35a4cdfb9bbde9ed83f6c04b..553ecc9b5631ffc0848a798bb3295f16d1722c1f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +@@ -30,6 +30,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/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/util/permissions/CommandPermissions.java b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +index 55695044da8363c8da040d922fa033c917f341d0..0617b9e067ce6a42209e1c4735b7a491defd945d 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..06fa43b469aea15dc64ce1866d08843327a34965 +--- /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 boolean afkBroadcastUseDisplayName = false; ++ 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); ++ afkBroadcastUseDisplayName = getBoolean("settings.messages.afk-broadcast-use-display-name", afkBroadcastUseDisplayName); ++ 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 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); ++ 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..a0f42ab3ce426ceffa1b60233d6418ff53349682 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +@@ -0,0 +1,3204 @@ ++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 double mendingMultiplier = 1.0; ++ 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); ++ mendingMultiplier = getDouble("gameplay-mechanics.mending-multiplier", mendingMultiplier); ++ 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 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 signAllowColors = false; ++ private void signSettings() { ++ 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); ++ batFollowRange = getDouble("mobs.bat.attributes.follow_range", batFollowRange); ++ batKnockbackResistance = getDouble("mobs.bat.attributes.knockback_resistance", batKnockbackResistance); ++ batMovementSpeed = getDouble("mobs.bat.attributes.movement_speed", batMovementSpeed); ++ batFlyingSpeed = getDouble("mobs.bat.attributes.flying_speed", batFlyingSpeed); ++ batArmor = getDouble("mobs.bat.attributes.armor", batArmor); ++ batArmorToughness = getDouble("mobs.bat.attributes.armor_toughness", batArmorToughness); ++ batAttackKnockback = getDouble("mobs.bat.attributes.attack_knockback", batAttackKnockback); ++ 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 boolean camelRidableInWater = false; ++ 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; ++ public int camelBreedingTicks = 6000; ++ private void camelSettings() { ++ camelRidableInWater = getBoolean("mobs.camel.ridable-in-water", camelRidableInWater); ++ 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", catDefaultCollarColor.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..992f8dfc628c7485e335191e1308cdfd4eedfbe8 +--- /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..ce614ae6b1fa0b31c1ee8dacb69134bb20c949f4 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/DolphinSpit.java +@@ -0,0 +1,106 @@ ++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)); ++ } ++ ++ // Purpur start ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ // Purpur end ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResult(this.position(), this, this::canHitEntity, mot, level()); ++ ++ 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..ea8b928b6d82689e71bbcc39ab497491072dfba6 +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/entity/PhantomFlames.java +@@ -0,0 +1,121 @@ ++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)); ++ } ++ ++ // Purpur start ++ @Override ++ public boolean canSaveToDisk() { ++ return false; ++ } ++ // Purpur end ++ ++ public void tick() { ++ super_tick(); ++ ++ Vec3 mot = this.getDeltaMovement(); ++ HitResult hitResult = ProjectileUtil.getHitResult(this.position(), this, this::canHitEntity, mot, level()); ++ ++ 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..9660716f4162a4441c6e1b0baddef8f5086566c5 +--- /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..2ebbaf5faa92a88bfb4d61298951e5b74157d1e1 +--- /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 70542f89d89db4b14b75aff07eccafa1680d317b..88a2f2fc72f81dbf372b27a06b376f5df4ce2873 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; +@@ -217,6 +218,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 ); +@@ -410,6 +412,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/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 675cd61221e807aadf28322b46c3daa1370241b5..ed4965b6db250090b0a9f6175e116bbe9c71fef3 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/0010-Bump-Dependencies.patch similarity index 95% rename from patches/server/0008-Bump-Dependencies.patch rename to patches/server/0010-Bump-Dependencies.patch index ad237a15..eb097157 100644 --- a/patches/server/0008-Bump-Dependencies.patch +++ b/patches/server/0010-Bump-Dependencies.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Bump Dependencies diff --git a/build.gradle.kts b/build.gradle.kts -index 9fa1085f85f4ab29024c5efc0507ed86da18d461..9d2546cdc9f231159b51bc80523073a26575c1b5 100644 +index f6ae0c843c6069ffedb1fb595b456e275cb81b4b..e8520363e96f2dc9d2a6b54dd2d3671175399628 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { @@ -52,7 +52,7 @@ index 9fa1085f85f4ab29024c5efc0507ed86da18d461..9d2546cdc9f231159b51bc80523073a2 isTransitive = false } // Paper end -@@ -56,11 +58,13 @@ dependencies { +@@ -60,11 +62,13 @@ dependencies { } // Pufferfish end @@ -69,7 +69,7 @@ index 9fa1085f85f4ab29024c5efc0507ed86da18d461..9d2546cdc9f231159b51bc80523073a2 } val craftbukkitPackageVersion = "1_20_R1" // Paper -@@ -226,3 +230,6 @@ tasks.registerRunTask("runDev") { +@@ -230,3 +234,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/0011-Remove-Mojang-username-check.patch similarity index 95% rename from patches/server/0009-Remove-Mojang-username-check.patch rename to patches/server/0011-Remove-Mojang-username-check.patch index 4c898f20..0945d1fb 100644 --- a/patches/server/0009-Remove-Mojang-username-check.patch +++ b/patches/server/0011-Remove-Mojang-username-check.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Remove Mojang username check diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -index 5b7c12db86be64433c65e31e3ecc0b444b0ddf48..cb8c4f91663c8653a1918a3864c17171c70ebe78 100644 +index 5ed89ce9d2c29927f48c1f7f8f9288f39d68b56c..30371f1a6dfb6fc170ac0cbdb3a7c78c837429c2 100644 --- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java @@ -37,6 +37,7 @@ import net.minecraft.util.Crypt; @@ -16,7 +16,7 @@ index 5b7c12db86be64433c65e31e3ecc0b444b0ddf48..cb8c4f91663c8653a1918a3864c17171 import org.galemc.gale.configuration.GaleGlobalConfiguration; import org.slf4j.Logger; -@@ -243,10 +244,10 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, +@@ -245,10 +246,10 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, public void handleHello(ServerboundHelloPacket packet) { // Gale start - JettPack - reduce array allocations Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet", ArrayConstants.emptyObjectArray); diff --git a/patches/server/0010-Remove-Spigot-Check-for-Broken-BungeeCord-Configurat.patch b/patches/server/0012-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/0012-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/0013-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/0013-Remove-Paper-s-Fix-tripwire-state-inconsistency.patch diff --git a/patches/server/0012-Remove-UseItemOnPacket-Too-Far-Check.patch b/patches/server/0014-Remove-UseItemOnPacket-Too-Far-Check.patch similarity index 95% rename from patches/server/0012-Remove-UseItemOnPacket-Too-Far-Check.patch rename to patches/server/0014-Remove-UseItemOnPacket-Too-Far-Check.patch index bf6cb5ff..efe94005 100644 --- a/patches/server/0012-Remove-UseItemOnPacket-Too-Far-Check.patch +++ b/patches/server/0014-Remove-UseItemOnPacket-Too-Far-Check.patch @@ -7,7 +7,7 @@ This Check is added in 1.17.x -> 1.18.x update by Mojang. By removing this check, it enable hackers to use some modules of hack clients. diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 8d90209a01020d44626f56e2cb0dd5eca300f699..e82898d6415db34d3aca01187000718ede37af21 100644 +index 0b3c784f6138db19594b443073430a9ec0dda052..0102343ea691c574eead8fb43bc132abeb05233a 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -189,6 +189,7 @@ import net.minecraft.world.phys.shapes.VoxelShape; @@ -18,7 +18,7 @@ index 8d90209a01020d44626f56e2cb0dd5eca300f699..e82898d6415db34d3aca01187000718e import org.slf4j.Logger; // CraftBukkit start -@@ -1980,7 +1981,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic +@@ -2046,7 +2047,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic Vec3 vec3d2 = vec3d.subtract(vec3d1); double d0 = 1.0000001D; diff --git a/patches/server/0013-KTP-Optimize-spigot-event-bus.patch b/patches/server/0015-KTP-Optimize-spigot-event-bus.patch similarity index 100% rename from patches/server/0013-KTP-Optimize-spigot-event-bus.patch rename to patches/server/0015-KTP-Optimize-spigot-event-bus.patch diff --git a/patches/server/0014-KeYi-Player-Skull-API.patch b/patches/server/0016-KeYi-Player-Skull-API.patch similarity index 84% rename from patches/server/0014-KeYi-Player-Skull-API.patch rename to patches/server/0016-KeYi-Player-Skull-API.patch index ad21e3a5..d03a361a 100644 --- a/patches/server/0014-KeYi-Player-Skull-API.patch +++ b/patches/server/0016-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/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 03bb444705916ffe0b9eb4b7496524dc3459ebe0..f4d3452fd2f22df2014df8bd721aa51f2a4d51bf 100644 +index a1bd3231001a57dd247d7e3fbb5b7e0bc590e58d..99243759c33e8fd187e64cfe95fcd2e7071a9b9f 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -25,6 +25,11 @@ import java.util.Optional; @@ -30,10 +30,10 @@ index 03bb444705916ffe0b9eb4b7496524dc3459ebe0..f4d3452fd2f22df2014df8bd721aa51f import org.bukkit.map.MapCursor; import org.bukkit.map.MapView; import org.bukkit.metadata.MetadataValue; -@@ -3138,4 +3144,28 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - return this.spigot; +@@ -3255,4 +3261,28 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket(getEntityId(), io.papermc.paper.adventure.PaperAdventure.asVanilla(message))); } - // Spigot end + // Purpur end + + // KeYi start + @Override diff --git a/patches/server/0015-KeYi-Disable-arrow-despawn-counter-by-default.patch b/patches/server/0017-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/0017-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/0018-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch similarity index 94% rename from patches/server/0016-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch rename to patches/server/0018-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch index 37c8f0eb..c46b5f23 100644 --- a/patches/server/0016-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch +++ b/patches/server/0018-KeYi-Add-an-option-for-spigot-item-merging-mechanism.patch @@ -7,7 +7,7 @@ Original license: MIT Original project: https://github.com/KeYiMC/KeYi diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -index fb0cd2ad93d2dbd678662f2cdad0851fd698cf3d..a45dd00d9cf335156d67d711f3ac5d6e6fe67314 100644 +index 18ca84f01d72c841341226ee8735f189cab1f3ed..261d174e437294bab96a4049172a30a27d2fed13 100644 --- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java @@ -38,6 +38,7 @@ import org.bukkit.event.entity.EntityPickupItemEvent; @@ -18,7 +18,7 @@ index fb0cd2ad93d2dbd678662f2cdad0851fd698cf3d..a45dd00d9cf335156d67d711f3ac5d6e public class ItemEntity extends Entity implements TraceableEntity { -@@ -330,7 +331,7 @@ public class ItemEntity extends Entity implements TraceableEntity { +@@ -336,7 +337,7 @@ public class ItemEntity extends Entity implements TraceableEntity { ItemStack itemstack1 = other.getItem(); if (Objects.equals(this.target, other.target) && ItemEntity.areMergable(itemstack, itemstack1)) { diff --git a/patches/server/0017-Carpet-Fixes-Optimized-getBiome-method.patch b/patches/server/0019-Carpet-Fixes-Optimized-getBiome-method.patch similarity index 100% rename from patches/server/0017-Carpet-Fixes-Optimized-getBiome-method.patch rename to patches/server/0019-Carpet-Fixes-Optimized-getBiome-method.patch diff --git a/patches/server/0018-Carpet-Fixes-Use-optimized-RecipeManager.patch b/patches/server/0020-Carpet-Fixes-Use-optimized-RecipeManager.patch similarity index 100% rename from patches/server/0018-Carpet-Fixes-Use-optimized-RecipeManager.patch rename to patches/server/0020-Carpet-Fixes-Use-optimized-RecipeManager.patch diff --git a/patches/server/0019-Petal-Reduce-sensor-work.patch b/patches/server/0021-Petal-Reduce-sensor-work.patch similarity index 55% rename from patches/server/0019-Petal-Reduce-sensor-work.patch rename to patches/server/0021-Petal-Reduce-sensor-work.patch index 6c29008c..5c3784d1 100644 --- a/patches/server/0019-Petal-Reduce-sensor-work.patch +++ b/patches/server/0021-Petal-Reduce-sensor-work.patch @@ -10,10 +10,10 @@ this patch is focused around the sensors used for ai delete the line of sight cache less often and use a faster nearby comparison diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 07ab60405dfbef46e74911a750ab0a9d067111c9..ef428fa6ae9897981f1c10f4344ec476d2787c03 100644 +index b65a3a09e96a7656d6372ecf5d7ba73e877121ec..4425e7a6ddd34c6b1ee841ff9b15050a770f5fc6 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -1024,12 +1024,14 @@ public abstract class LivingEntity extends Entity implements Attackable { +@@ -1037,22 +1037,24 @@ public abstract class LivingEntity extends Entity implements Attackable { } if (entity != null) { @@ -22,19 +22,32 @@ index 07ab60405dfbef46e74911a750ab0a9d067111c9..ef428fa6ae9897981f1c10f4344ec476 + //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)) { -+ if (entitytypes == EntityType.SKELETON && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.SKELETON_SKULL) || entitytypes == EntityType.ZOMBIE && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.ZOMBIE_HEAD) || entitytypes == EntityType.PIGLIN && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.PIGLIN_HEAD) || entitytypes == EntityType.PIGLIN_BRUTE && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.PIGLIN_HEAD) || entitytypes == EntityType.CREEPER && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.CREEPER_HEAD)) { - d0 *= 0.5D; + // Purpur start +- if (entitytypes == EntityType.SKELETON && itemstack.is(Items.SKELETON_SKULL)) { ++ if (entitytypes == EntityType.SKELETON && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.SKELETON_SKULL)) { + d0 *= entity.level().purpurConfig.skeletonHeadVisibilityPercent; + } +- else if (entitytypes == EntityType.ZOMBIE && itemstack.is(Items.ZOMBIE_HEAD)) { ++ else if (entitytypes == EntityType.ZOMBIE && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.ZOMBIE_HEAD)) { + d0 *= entity.level().purpurConfig.zombieHeadVisibilityPercent; + } +- else if (entitytypes == EntityType.CREEPER && itemstack.is(Items.CREEPER_HEAD)) { ++ else if (entitytypes == EntityType.CREEPER && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.CREEPER_HEAD)) { + d0 *= entity.level().purpurConfig.creeperHeadVisibilityPercent; + } +- else if ((entitytypes == EntityType.PIGLIN || entitytypes == EntityType.PIGLIN_BRUTE) && itemstack.is(Items.PIGLIN_HEAD)) { ++ else if ((entitytypes == EntityType.PIGLIN || entitytypes == EntityType.PIGLIN_BRUTE) && this.getItemBySlot(EquipmentSlot.HEAD).is(Items.PIGLIN_HEAD)) { + d0 *= entity.level().purpurConfig.piglinHeadVisibilityPercent; } + // petal end - } + // Purpur end - return d0; + // Purpur start diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index 1674f9accbbbb9ecdd99f05da6032398c4d82b38..7cc725b2f4feb3dcbe2fd0556954ab601fbd6e53 100644 +index 29fc0a756c4541bb5292c74edef983c4fad373f1..c675af50ca8ca221982355c87603df3f4da02513 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -902,8 +902,8 @@ public abstract class Mob extends LivingEntity implements Targeting { +@@ -931,8 +931,8 @@ public abstract class Mob extends LivingEntity implements Targeting { return; } // Paper end); diff --git a/patches/server/0020-Akarin-Save-Json-list-asynchronously.patch b/patches/server/0022-Akarin-Save-Json-list-asynchronously.patch similarity index 100% rename from patches/server/0020-Akarin-Save-Json-list-asynchronously.patch rename to patches/server/0022-Akarin-Save-Json-list-asynchronously.patch diff --git a/patches/server/0021-Slice-Smooth-Teleports.patch b/patches/server/0023-Slice-Smooth-Teleports.patch similarity index 82% rename from patches/server/0021-Slice-Smooth-Teleports.patch rename to patches/server/0023-Slice-Smooth-Teleports.patch index ae25cad2..baa29fe2 100644 --- a/patches/server/0021-Slice-Smooth-Teleports.patch +++ b/patches/server/0023-Slice-Smooth-Teleports.patch @@ -7,22 +7,22 @@ Original license: MIT Original project: https://github.com/Cryptite/Slice diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 00ce8b78e35055da01a053cb0d4ed914d50acebc..853ca649d513d263482dd56b84cf9c55f5972f37 100644 +index 227368ac352e14f4d9f32d3847162cff13f5cf9b..7dd1d1162844e0f1fe0eab022800d910ce1e5a7d 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -278,6 +278,7 @@ 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 +@@ -283,6 +283,7 @@ public class ServerPlayer extends Player { + private boolean ramBar = false; // Purpur + private boolean tpsBar = false; // Purpur + private boolean compassBar = false; // Purpur + public boolean smoothWorldTeleport; // Slice private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1)); public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 697bd3a7c0ab1e941355a818bffe85bdb28a70db..326bbc8700249f80b57d059952beea861e4bfd35 100644 +index 5ecf6b85a909ea8934f50e0dbe262a4f409a855f..7d6fc02b918eeb2f3c0a73f03a461f021112b9f6 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -942,12 +942,12 @@ public abstract class PlayerList { +@@ -944,12 +944,12 @@ public abstract class PlayerList { int i = flag ? 1 : 0; // CraftBukkit start LevelData worlddata = worldserver1.getLevelData(); @@ -38,10 +38,10 @@ index 697bd3a7c0ab1e941355a818bffe85bdb28a70db..326bbc8700249f80b57d059952beea86 // entityplayer1.connection.teleport(entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot()); diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index f4d3452fd2f22df2014df8bd721aa51f2a4d51bf..747127ae9514363622375cee55b3fd9f80776739 100644 +index 99243759c33e8fd187e64cfe95fcd2e7071a9b9f..a5857f78bd44cb5f42d2721669d92f1301a0da4a 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -1232,6 +1232,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { +@@ -1237,6 +1237,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { // Paper end } diff --git a/patches/server/0022-PaperPR-Optimize-VarInts.patch b/patches/server/0024-PaperPR-Optimize-VarInts.patch similarity index 90% rename from patches/server/0022-PaperPR-Optimize-VarInts.patch rename to patches/server/0024-PaperPR-Optimize-VarInts.patch index 0cee0253..d18a5140 100644 --- a/patches/server/0022-PaperPR-Optimize-VarInts.patch +++ b/patches/server/0024-PaperPR-Optimize-VarInts.patch @@ -8,10 +8,10 @@ Original project: https://github.com/PaperMC/Velocity Paper pull request: https://github.com/PaperMC/Paper/pull/8418 diff --git a/src/main/java/net/minecraft/network/FriendlyByteBuf.java b/src/main/java/net/minecraft/network/FriendlyByteBuf.java -index 9938bb90bef84cf784f9a1ceb02a1a45aa8b48a1..f5080a3716a65a7175b043c41f1e002322ecbf70 100644 +index 1f4b64a5f812376c499c98cb4be62469bd0b7dbe..1f4320d8243cc64b86892c23c601d14101d5a7ad 100644 --- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java +++ b/src/main/java/net/minecraft/network/FriendlyByteBuf.java -@@ -103,6 +103,18 @@ public class FriendlyByteBuf extends ByteBuf { +@@ -105,6 +105,18 @@ public class FriendlyByteBuf extends ByteBuf { } public static int getVarIntSize(int value) { @@ -30,7 +30,7 @@ index 9938bb90bef84cf784f9a1ceb02a1a45aa8b48a1..f5080a3716a65a7175b043c41f1e0023 for (int j = 1; j < 5; ++j) { if ((value & -1 << j * 7) == 0) { return j; -@@ -613,6 +625,21 @@ public class FriendlyByteBuf extends ByteBuf { +@@ -615,6 +627,21 @@ public class FriendlyByteBuf extends ByteBuf { } public FriendlyByteBuf writeVarInt(int value) { diff --git a/patches/server/0023-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/0023-Parchment-Make-FixLight-use-action-bar.patch rename to patches/server/0025-Parchment-Make-FixLight-use-action-bar.patch diff --git a/patches/server/0024-Leaves-Server-Utils.patch b/patches/server/0026-Leaves-Server-Utils.patch similarity index 94% rename from patches/server/0024-Leaves-Server-Utils.patch rename to patches/server/0026-Leaves-Server-Utils.patch index 8c5efb2e..67fa7b8c 100644 --- a/patches/server/0024-Leaves-Server-Utils.patch +++ b/patches/server/0026-Leaves-Server-Utils.patch @@ -33,29 +33,29 @@ index 46954db7ecd35ac4018fdf476df7c8020d7ce6c8..044c51ebb058fc36074fd178929e3279 public PlayerAreaMap() { super(); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 7652900f88fe778c3855536e8744445704a1f8b1..b34be55f820834ea973a1a007bc8c897d4331db8 100644 +index 8c1cf9f56a13cae026a18d14b8ac3b884f7d86ca..33820dff0751a84d9bc630c29847382aadb88af9 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -407,6 +407,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - private UUID originWorld; +@@ -408,6 +408,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { public boolean freezeLocked = false; // Paper - Freeze Tick Lock API public boolean collidingWithWorldBorder; // Paper + public @Nullable Boolean immuneToFire = null; // Purpur - Fire immune API + private CompoundTag leavesData = new CompoundTag(); // Leaves - Leaves ex data public void setOrigin(@javax.annotation.Nonnull Location location) { this.origin = location.toVector(); -@@ -2481,6 +2482,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - nbt.putBoolean("Paper.FreezeLock", true); +@@ -2523,6 +2524,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + nbt.putBoolean("Purpur.FireImmune", immuneToFire); } - // Paper end + // Purpur end + nbt.put("Leaves.Data", leavesData); // Leaves - leaves ex data return nbt; } catch (Throwable throwable) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); -@@ -2649,6 +2651,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - freezeLocked = nbt.getBoolean("Paper.FreezeLock"); +@@ -2696,6 +2698,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + immuneToFire = nbt.getBoolean("Purpur.FireImmune"); } - // Paper end + // Purpur end + // Leaves start - leaves ex data + if (nbt.contains("Leaves.Data")) { + leavesData = nbt.getCompound("Leaves.Data"); @@ -64,10 +64,10 @@ index 7652900f88fe778c3855536e8744445704a1f8b1..b34be55f820834ea973a1a007bc8c897 } catch (Throwable throwable) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); -@@ -4899,4 +4906,10 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - return ((net.minecraft.server.level.ServerChunkCache) level.getChunkSource()).isPositionTicking(this); +@@ -5025,4 +5032,10 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return false; } - // Paper end + // Purpur end + + // Leaves start - leaves ex data + public CompoundTag getLeavesData() { diff --git a/patches/server/0025-Leaves-Jade-Protocol.patch b/patches/server/0027-Leaves-Jade-Protocol.patch similarity index 97% rename from patches/server/0025-Leaves-Jade-Protocol.patch rename to patches/server/0027-Leaves-Jade-Protocol.patch index 51ff53ec..8539e32a 100644 --- a/patches/server/0025-Leaves-Jade-Protocol.patch +++ b/patches/server/0027-Leaves-Jade-Protocol.patch @@ -9,7 +9,7 @@ Original project: https://github.com/LeavesMC/Leaves This patch is Powered by Jade(https://github.com/Snownee/Jade) diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index e82898d6415db34d3aca01187000718ede37af21..48d34b8a2a86225942cb25d681c334abdeeae154 100644 +index 0102343ea691c574eead8fb43bc132abeb05233a..946ffe66ff8187dfb7876187f6666c1afb19c297 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -249,6 +249,7 @@ import org.bukkit.inventory.CraftingInventory; @@ -20,7 +20,7 @@ index e82898d6415db34d3aca01187000718ede37af21..48d34b8a2a86225942cb25d681c334ab // CraftBukkit end public class ServerGamePacketListenerImpl implements ServerPlayerConnection, TickablePacketListener, ServerGamePacketListener { -@@ -3595,6 +3596,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic +@@ -3688,6 +3689,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // Paper end this.cserver.getMessenger().dispatchIncomingMessage(this.player.getBukkitEntity(), packet.identifier.toString(), data); @@ -31,7 +31,7 @@ index e82898d6415db34d3aca01187000718ede37af21..48d34b8a2a86225942cb25d681c334ab ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t dispatch custom payload", ex); this.disconnect("Invalid custom payload!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 326bbc8700249f80b57d059952beea861e4bfd35..9ad8b5dccb4627f647d9e993b72bb0dd5793999b 100644 +index 7d6fc02b918eeb2f3c0a73f03a461f021112b9f6..5a9ca4cd26179048850b1f16c635480cdafccb75 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -358,6 +358,8 @@ public abstract class PlayerList { @@ -44,10 +44,10 @@ index 326bbc8700249f80b57d059952beea861e4bfd35..9ad8b5dccb4627f647d9e993b72bb0dd if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure 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 c83dabddf93249a6477c10725622119c939db4d5..bfb85d119355f99c96e8cebb0c9be43c6be4b6b4 100644 +index e91b4d63d42276f8a498cab7c439c785730f3f6f..a2d7c5008c6d6716d9530e00de0db0276cc0d9fc 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 -@@ -251,7 +251,7 @@ public class Tadpole extends AbstractFish { +@@ -288,7 +288,7 @@ public class Tadpole extends AbstractFish { } @@ -57,7 +57,7 @@ index c83dabddf93249a6477c10725622119c939db4d5..bfb85d119355f99c96e8cebb0c9be43c } diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index bca065a167a1bf0581559f2e578c112fdf570c8a..48aa2507667a46704dfc6f17711b4020325396b6 100644 +index 5fabbef99fe1c38a4b52461d9c107c51ecb9b6e5..fec071cf14e8bfa42b70b9b4bdad5414fd4df3cc 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -253,6 +253,7 @@ import org.bukkit.scoreboard.Criteria; @@ -68,7 +68,7 @@ index bca065a167a1bf0581559f2e578c112fdf570c8a..48aa2507667a46704dfc6f17711b4020 import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; -@@ -392,6 +393,7 @@ public final class CraftServer implements Server { +@@ -406,6 +407,7 @@ public final class CraftServer implements Server { MapPalette.setMapColorCache(new CraftMapColorCache(this.logger)); } datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper @@ -76,10 +76,10 @@ index bca065a167a1bf0581559f2e578c112fdf570c8a..48aa2507667a46704dfc6f17711b4020 } public boolean getCommandBlockOverride(String command) { -@@ -984,6 +986,11 @@ public final class CraftServer implements Server { - org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot +@@ -999,6 +1001,11 @@ public final class CraftServer implements Server { this.console.paperConfigurations.reloadConfigs(this.console); this.console.galeConfigurations.reloadConfigs(this.console); // Gale - Gale configuration + org.purpurmc.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur + // Leaves start - Jade + if (LeafConfig.jadeProtocol) { + top.leavesmc.leaves.protocol.JadeProtocol.enableAllPlayer(); diff --git a/patches/server/0026-Leaves-Appleskin-Protocol.patch b/patches/server/0028-Leaves-Appleskin-Protocol.patch similarity index 93% rename from patches/server/0026-Leaves-Appleskin-Protocol.patch rename to patches/server/0028-Leaves-Appleskin-Protocol.patch index 23b211c5..c5c2cc42 100644 --- a/patches/server/0026-Leaves-Appleskin-Protocol.patch +++ b/patches/server/0028-Leaves-Appleskin-Protocol.patch @@ -7,10 +7,10 @@ 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 c62a225a97deca88a15f3d7c61550debdcbfccfe..dc7185ceaad06fefdf2df6afe19eb183c250fbaf 100644 +index c21020ff14ea68d4638278aad0641a3119b2fdc8..d07a1760fe6ed8003a399488b40b34e09b36e32a 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1546,6 +1546,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop - + diff --git a/patches/server/0029-Fix-compile-error.patch b/patches/server/0031-Fix-compile-error.patch similarity index 100% rename from patches/server/0029-Fix-compile-error.patch rename to patches/server/0031-Fix-compile-error.patch